From ff4171a9ca7e54288d9adeec1a815bd55762b834 Mon Sep 17 00:00:00 2001 From: Peter Empen Date: Wed, 21 Feb 2024 19:36:33 +0100 Subject: [PATCH 001/142] sphere-mongo-derivation-scala-3 --- build.sbt | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 8fc9351e..e01fac33 100644 --- a/build.sbt +++ b/build.sbt @@ -1,7 +1,11 @@ import pl.project13.scala.sbt.JmhPlugin +lazy val scala2_12 = "2.12.18" +lazy val scala2_13 = "2.13.12" +lazy val scala3 = "3.3.2" + // sbt-github-actions needs configuration in `ThisBuild` -ThisBuild / crossScalaVersions := Seq("2.12.18", "2.13.12") +ThisBuild / crossScalaVersions := Seq(scala2_12, scala2_13, scala3) ThisBuild / scalaVersion := crossScalaVersions.value.last ThisBuild / githubWorkflowPublishTargetBranches := List() ThisBuild / githubWorkflowJavaVersions := List(JavaSpec.temurin("17")) @@ -107,10 +111,17 @@ lazy val `sphere-mongo-core` = project lazy val `sphere-mongo-derivation` = project .in(file("./mongo/mongo-derivation")) + .settings(crossScalaVersions := Seq(scala2_12, scala2_13)) .settings(standardSettings: _*) .settings(Fmpp.settings: _*) .dependsOn(`sphere-mongo-core`) +lazy val `sphere-mongo-derivation-scala-3` = project + .settings(crossScalaVersions := Seq(scala3)) + .in(file("./mongo/mongo-derivation-scala-3")) + .settings(standardSettings: _*) + .dependsOn(`sphere-mongo-core`) + lazy val `sphere-mongo-derivation-magnolia` = project .in(file("./mongo/mongo-derivation-magnolia")) .settings(standardSettings: _*) From 6aab3f9b7d83602f380f303874f02468160bcddc Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 22 Feb 2024 10:06:59 +0100 Subject: [PATCH 002/142] sphere-mongo-core, sphere-util compiles with both Scala 2 and 3 --- .../io/sphere/mongo/format/DefaultMongoFormats.scala | 8 ++++---- .../io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala | 2 +- .../io/sphere/mongo/format/DefaultMongoFormatsTest.scala | 4 ++-- util/dependencies.sbt | 2 +- util/src/main/scala/Money.scala | 7 ++++--- util/src/test/scala/DomainObjectsGen.scala | 2 +- util/src/test/scala/HighPrecisionMoneySpec.scala | 3 ++- util/src/test/scala/MoneySpec.scala | 3 ++- 8 files changed, 17 insertions(+), 14 deletions(-) diff --git a/mongo/mongo-core/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala b/mongo/mongo-core/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala index a2a8918e..6aecbcb3 100644 --- a/mongo/mongo-core/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala +++ b/mongo/mongo-core/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala @@ -50,7 +50,7 @@ trait DefaultMongoFormats { implicit def optionFormat[@specialized A](implicit f: MongoFormat[A]): MongoFormat[Option[A]] = new MongoFormat[Option[A]] { - import scala.collection.JavaConverters._ + import scala.jdk.CollectionConverters._ override def toMongoValue(a: Option[A]) = a match { case Some(aa) => f.toMongoValue(aa) case None => MongoNothing @@ -78,7 +78,7 @@ trait DefaultMongoFormats { implicit def vecFormat[@specialized A](implicit f: MongoFormat[A]): MongoFormat[Vector[A]] = new MongoFormat[Vector[A]] { - import scala.collection.JavaConverters._ + import scala.jdk.CollectionConverters._ override def toMongoValue(a: Vector[A]) = { val m = new BasicBSONList() if (a.nonEmpty) m.addAll(a.map(f.toMongoValue(_).asInstanceOf[AnyRef]).asJavaCollection) @@ -103,7 +103,7 @@ trait DefaultMongoFormats { implicit def listFormat[@specialized A](implicit f: MongoFormat[A]): MongoFormat[List[A]] = new MongoFormat[List[A]] { - import scala.collection.JavaConverters._ + import scala.jdk.CollectionConverters._ override def toMongoValue(a: List[A]) = { val m = new BasicBSONList() if (a.nonEmpty) m.addAll(a.map(f.toMongoValue(_).asInstanceOf[AnyRef]).asJavaCollection) @@ -128,7 +128,7 @@ trait DefaultMongoFormats { implicit def setFormat[@specialized A](implicit f: MongoFormat[A]): MongoFormat[Set[A]] = new MongoFormat[Set[A]] { - import scala.collection.JavaConverters._ + import scala.jdk.CollectionConverters._ override def toMongoValue(a: Set[A]) = { val m = new BasicBSONList() if (a.nonEmpty) m.addAll(a.map(f.toMongoValue(_).asInstanceOf[AnyRef]).asJavaCollection) diff --git a/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala b/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala index 27ab0fd0..9638fd06 100644 --- a/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala +++ b/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala @@ -9,7 +9,7 @@ import org.bson.BSONObject import org.scalatest.wordspec.AnyWordSpec import org.scalatest.matchers.should.Matchers -import scala.collection.JavaConverters._ +import scala.jdk.CollectionConverters._ class BaseMoneyMongoFormatTest extends AnyWordSpec with Matchers { diff --git a/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/DefaultMongoFormatsTest.scala b/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/DefaultMongoFormatsTest.scala index 15d3673b..00437f8a 100644 --- a/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/DefaultMongoFormatsTest.scala +++ b/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/DefaultMongoFormatsTest.scala @@ -12,7 +12,7 @@ import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks -import scala.collection.JavaConverters._ +import scala.jdk.CollectionConverters._ object DefaultMongoFormatsTest { case class User(name: String) @@ -126,7 +126,7 @@ class DefaultMongoFormatsTest } "support Java Locale" in { - Locale.getAvailableLocales.filter(_.toLanguageTag != LangTag.UNDEFINED).foreach { l: Locale => + Locale.getAvailableLocales.filter(_.toLanguageTag != LangTag.UNDEFINED).foreach { l => localeFormat.fromMongoValue(localeFormat.toMongoValue(l)).toLanguageTag must be( l.toLanguageTag) } diff --git a/util/dependencies.sbt b/util/dependencies.sbt index 6417ca83..68f1ae8a 100644 --- a/util/dependencies.sbt +++ b/util/dependencies.sbt @@ -2,6 +2,6 @@ libraryDependencies ++= Seq( "com.typesafe.scala-logging" %% "scala-logging" % "3.9.4", "joda-time" % "joda-time" % "2.12.7", "org.joda" % "joda-convert" % "2.2.3", - "org.typelevel" %% "cats-core" % "2.10.0", + ("org.typelevel" % "cats-core" % "2.10.0").cross(CrossVersion.binary), "org.json4s" %% "json4s-scalap" % "4.0.7" ) diff --git a/util/src/main/scala/Money.scala b/util/src/main/scala/Money.scala index 7b627050..b3ffdac7 100644 --- a/util/src/main/scala/Money.scala +++ b/util/src/main/scala/Money.scala @@ -64,8 +64,8 @@ object BaseMoney { def requireSameCurrency(m1: BaseMoney, m2: BaseMoney): Unit = require(m1.currency eq m2.currency, s"${m1.currency} != ${m2.currency}") - def toScalaRoundingMode(mode: java.math.RoundingMode): RoundingMode = - BigDecimal.RoundingMode(mode.ordinal()) + def toScalaRoundingMode(mode: java.math.RoundingMode): RoundingMode.Value = + BigDecimal.RoundingMode(mode.ordinal) implicit def baseMoneyMonoid(implicit c: Currency, mode: RoundingMode): Monoid[BaseMoney] = new Monoid[BaseMoney] { @@ -90,7 +90,7 @@ object BaseMoney { * @param currency * The currency of the amount. */ -case class Money private (centAmount: Long, currency: Currency) +case class Money private[util] (centAmount: Long, currency: Currency) extends BaseMoney with Ordered[Money] { import Money._ @@ -263,6 +263,7 @@ object Money { } def apply(amount: BigDecimal, currency: Currency): Money = { + println("this is called") require( amount.scale == currency.getDefaultFractionDigits, "The scale of the given amount does not match the scale of the provided currency." + diff --git a/util/src/test/scala/DomainObjectsGen.scala b/util/src/test/scala/DomainObjectsGen.scala index 97536bfd..b654f020 100644 --- a/util/src/test/scala/DomainObjectsGen.scala +++ b/util/src/test/scala/DomainObjectsGen.scala @@ -4,7 +4,7 @@ import java.util.Currency import org.scalacheck.Gen -import scala.collection.JavaConverters._ +import scala.jdk.CollectionConverters._ object DomainObjectsGen { diff --git a/util/src/test/scala/HighPrecisionMoneySpec.scala b/util/src/test/scala/HighPrecisionMoneySpec.scala index e8a539b2..68493174 100644 --- a/util/src/test/scala/HighPrecisionMoneySpec.scala +++ b/util/src/test/scala/HighPrecisionMoneySpec.scala @@ -9,12 +9,13 @@ import org.scalatest.matchers.must.Matchers import scala.collection.mutable.ArrayBuffer import scala.language.postfixOps +import scala.math.BigDecimal class HighPrecisionMoneySpec extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks { import HighPrecisionMoney.ImplicitsString._ import HighPrecisionMoney.ImplicitsStringPrecise._ - implicit val defaultRoundingMode = BigDecimal.RoundingMode.HALF_EVEN + implicit val defaultRoundingMode: BigDecimal.RoundingMode.Value = BigDecimal.RoundingMode.HALF_EVEN val Euro: Currency = Currency.getInstance("EUR") diff --git a/util/src/test/scala/MoneySpec.scala b/util/src/test/scala/MoneySpec.scala index 718d6e19..7d4c1a4a 100644 --- a/util/src/test/scala/MoneySpec.scala +++ b/util/src/test/scala/MoneySpec.scala @@ -5,12 +5,13 @@ import org.scalatest.matchers.must.Matchers import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks import scala.language.postfixOps +import scala.math.BigDecimal class MoneySpec extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks { import Money.ImplicitsDecimal._ import Money._ - implicit val mode = BigDecimal.RoundingMode.UNNECESSARY + implicit val mode: BigDecimal.RoundingMode.Value = BigDecimal.RoundingMode.UNNECESSARY def euroCents(cents: Long): Money = EUR(0).withCentAmount(cents) From 91424e0f1a4720a4aaca6be5ffb117e5107ee172 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 22 Feb 2024 11:29:06 +0100 Subject: [PATCH 003/142] some macro test code --- .../io/sphere/mongo/generic/generic.scala | 30 +++++++++ .../src/test/scala/SerializationTest.scala | 65 +++++++++++++++++++ .../scala/io/sphere/mongo/generic/Test.scala | 14 ++++ 3 files changed, 109 insertions(+) create mode 100644 mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala create mode 100644 mongo/mongo-derivation-scala-3/src/test/scala/SerializationTest.scala create mode 100644 mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/Test.scala diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala new file mode 100644 index 00000000..bf3319a4 --- /dev/null +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala @@ -0,0 +1,30 @@ +package io.sphere.mongo.generic + +import io.sphere.mongo.format.MongoFormat + +import scala.quoted.* // imports Quotes, Expr + +inline def deriveMongoFormat[A]: MongoFormat[A] = ${ deriveMongoFormatImpl } + +def deriveMongoFormatImpl[A](using Type[A], Quotes): Expr[MongoFormat[A]] = { + '{ dummyFormat[A] } +} + +def dummyFormat[A]: MongoFormat[A] = new MongoFormat[A]: + override def toMongoValue(a: A): Any = ??? + override def fromMongoValue(any: Any): A = ??? + + +def mongoEnum(e: Enumeration): MongoFormat[e.Value] = new MongoFormat[e.Value] { + def toMongoValue(a: e.Value): Any = a.toString + def fromMongoValue(any: Any): e.Value = e.withName(any.asInstanceOf[String]) +} + + + + +def inspectCode(x: Expr[Any])(using Quotes): Expr[Any] = + println(x.show) + x + +inline def inspect(inline x: Any): Any = ${ inspectCode('x) } \ No newline at end of file diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/SerializationTest.scala b/mongo/mongo-derivation-scala-3/src/test/scala/SerializationTest.scala new file mode 100644 index 00000000..8857227c --- /dev/null +++ b/mongo/mongo-derivation-scala-3/src/test/scala/SerializationTest.scala @@ -0,0 +1,65 @@ +package io.sphere.mongo + +import com.mongodb.{BasicDBObject, DBObject} +import io.sphere.mongo.format.DefaultMongoFormats.* +import io.sphere.mongo.format.MongoFormat +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +object SerializationTest { +// case class Something(a: Option[Int], b: Int = 2) + + object Color extends Enumeration { + val Blue, Red, Yellow = Value + } +} + +class SerializationTest extends AnyWordSpec with Matchers { + import SerializationTest.Color + +// "mongoProduct" must { +// "deserialize mongo object" in { +// val dbo = new BasicDBObject() +// dbo.put("a", Integer.valueOf(3)) +// dbo.put("b", Integer.valueOf(4)) +// +// val mongoFormat: MongoFormat[Something] = io.sphere.mongo.generic.deriveMongoFormat +// val something = mongoFormat.fromMongoValue(dbo) +// something must be(Something(Some(3), 4)) +// } +// +// "generate a format that serializes optional fields with value None as BSON objects without that field" in { +// val testFormat: MongoFormat[Something] = +// io.sphere.mongo.generic.mongoProduct[Something, Option[Int], Int] { +// (a: Option[Int], b: Int) => Something(a, b) +// } +// +// val serializedObject = testFormat.toMongoValue(Something(None, 1)).asInstanceOf[DBObject] +// serializedObject.keySet().contains("b") must be(true) +// serializedObject.keySet().contains("a") must be(false) +// } +// +// "generate a format that use default values" in { +// val dbo = new BasicDBObject() +// dbo.put("a", Integer.valueOf(3)) +// +// val mongoFormat: MongoFormat[Something] = io.sphere.mongo.generic.deriveMongoFormat +// val something = mongoFormat.fromMongoValue(dbo) +// something must be(Something(Some(3), 2)) +// } +// } + + "mongoEnum" must { + "serialize and deserialize enums" in { + val mongo: MongoFormat[Color.Value] = io.sphere.mongo.generic.deriveMongoFormat[Color.Value] + + // mongo java driver knows how to encode/decode Strings + val serializedObject = mongo.toMongoValue(Color.Red).asInstanceOf[String] + serializedObject must be("Red") + + val enumValue = mongo.fromMongoValue(serializedObject) + enumValue must be(Color.Red) + } + } + +} diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/Test.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/Test.scala new file mode 100644 index 00000000..eb30a764 --- /dev/null +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/Test.scala @@ -0,0 +1,14 @@ +package io.sphere.mongo.generic + +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class Test extends AnyWordSpec with Matchers: + case class A(x: Int) + + "asd" must { + inspect(A(324535)) + () + } + +end Test \ No newline at end of file From 04a2d7517c04446729896d3f761567276af475b2 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 22 Feb 2024 12:54:27 +0100 Subject: [PATCH 004/142] some macro test code2 --- .../io/sphere/mongo/generic/generic.scala | 28 +++++++++++++++++-- .../src/test/scala/SerializationTest.scala | 9 ++++-- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala index bf3319a4..e232c189 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala @@ -2,14 +2,30 @@ package io.sphere.mongo.generic import io.sphere.mongo.format.MongoFormat -import scala.quoted.* // imports Quotes, Expr +import scala.quoted.* inline def deriveMongoFormat[A]: MongoFormat[A] = ${ deriveMongoFormatImpl } def deriveMongoFormatImpl[A](using Type[A], Quotes): Expr[MongoFormat[A]] = { - '{ dummyFormat[A] } + val q = summon[Quotes] + import q.reflect.* + val t = TypeRepr.of[A] + + + + if(t <:< TypeRepr.of[Enumeration#Value]) then +// Apply( +// '{io.sphere.mongo.generic} +// Select(Select(Select(Select(Select(Ident("io"), "sphere"), "mongo"), "generic"), "generic$package"), "mongoEnum"), List(Ident(t.asTerm)) +// ) + '{ dummyFormat[A] } + else + println(".......") + '{ dummyFormat[A] } } +//Apply(Select(Select(Select(Select(Select(Ident("io"), "sphere"), "mongo"), "generic"), "generic$package"), "mongoEnum"), List(Ident("Color"))) + def dummyFormat[A]: MongoFormat[A] = new MongoFormat[A]: override def toMongoValue(a: A): Any = ??? override def fromMongoValue(any: Any): A = ??? @@ -24,7 +40,13 @@ def mongoEnum(e: Enumeration): MongoFormat[e.Value] = new MongoFormat[e.Value] { def inspectCode(x: Expr[Any])(using Quotes): Expr[Any] = - println(x.show) +// val qq = summon[Quotes] +// import qq.reflect.* + import quotes.reflect.* + + val tree: Tree = x.asTerm + + println(s"----- ${tree.show(using Printer.TreeStructure)}") x inline def inspect(inline x: Any): Any = ${ inspectCode('x) } \ No newline at end of file diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/SerializationTest.scala b/mongo/mongo-derivation-scala-3/src/test/scala/SerializationTest.scala index 8857227c..6989fe70 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/SerializationTest.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/SerializationTest.scala @@ -7,7 +7,7 @@ import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec object SerializationTest { -// case class Something(a: Option[Int], b: Int = 2) + case class Something(a: Option[Int], b: Int = 2) object Color extends Enumeration { val Blue, Red, Yellow = Value @@ -15,7 +15,7 @@ object SerializationTest { } class SerializationTest extends AnyWordSpec with Matchers { - import SerializationTest.Color + import SerializationTest.* // "mongoProduct" must { // "deserialize mongo object" in { @@ -51,7 +51,12 @@ class SerializationTest extends AnyWordSpec with Matchers { "mongoEnum" must { "serialize and deserialize enums" in { + + io.sphere.mongo.generic.inspect(io.sphere.mongo.generic.mongoEnum(Color)) + val mongo: MongoFormat[Color.Value] = io.sphere.mongo.generic.deriveMongoFormat[Color.Value] + + // mongo java driver knows how to encode/decode Strings val serializedObject = mongo.toMongoValue(Color.Red).asInstanceOf[String] From bbabb8a90411d53e5ca98253b7c1d929a71bf89e Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 22 Feb 2024 15:08:55 +0100 Subject: [PATCH 005/142] some macro test code2 --- .../io/sphere/mongo/generic/generic.scala | 30 +++++-------------- .../sphere/mongo}/SerializationTest.scala | 7 +---- .../sphere/mongo/generic/TempUtilMacros.scala | 15 ++++++++++ .../scala/io/sphere/mongo/generic/Test.scala | 14 --------- 4 files changed, 24 insertions(+), 42 deletions(-) rename mongo/mongo-derivation-scala-3/src/test/scala/{ => io/sphere/mongo}/SerializationTest.scala (94%) create mode 100644 mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/TempUtilMacros.scala delete mode 100644 mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/Test.scala diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala index e232c189..49966d43 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala @@ -9,16 +9,16 @@ inline def deriveMongoFormat[A]: MongoFormat[A] = ${ deriveMongoFormatImpl } def deriveMongoFormatImpl[A](using Type[A], Quotes): Expr[MongoFormat[A]] = { val q = summon[Quotes] import q.reflect.* - val t = TypeRepr.of[A] + val typeRepr = TypeRepr.of[A] + if(typeRepr <:< TypeRepr.of[Enumeration#Value]) then + val TypeRef(enumTerm @ TermRef(_, _), _) = typeRepr - - if(t <:< TypeRepr.of[Enumeration#Value]) then -// Apply( -// '{io.sphere.mongo.generic} -// Select(Select(Select(Select(Select(Ident("io"), "sphere"), "mongo"), "generic"), "generic$package"), "mongoEnum"), List(Ident(t.asTerm)) -// ) - '{ dummyFormat[A] } + val mongoEnumCall = Apply( + Ref(Symbol.requiredMethod("io.sphere.mongo.generic.mongoEnum")), + List(Ident(enumTerm)) + ) + mongoEnumCall.asExprOf[MongoFormat[A]] else println(".......") '{ dummyFormat[A] } @@ -36,17 +36,3 @@ def mongoEnum(e: Enumeration): MongoFormat[e.Value] = new MongoFormat[e.Value] { def fromMongoValue(any: Any): e.Value = e.withName(any.asInstanceOf[String]) } - - - -def inspectCode(x: Expr[Any])(using Quotes): Expr[Any] = -// val qq = summon[Quotes] -// import qq.reflect.* - import quotes.reflect.* - - val tree: Tree = x.asTerm - - println(s"----- ${tree.show(using Printer.TreeStructure)}") - x - -inline def inspect(inline x: Any): Any = ${ inspectCode('x) } \ No newline at end of file diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/SerializationTest.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala similarity index 94% rename from mongo/mongo-derivation-scala-3/src/test/scala/SerializationTest.scala rename to mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala index 6989fe70..bb1df326 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/SerializationTest.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala @@ -51,12 +51,7 @@ class SerializationTest extends AnyWordSpec with Matchers { "mongoEnum" must { "serialize and deserialize enums" in { - - io.sphere.mongo.generic.inspect(io.sphere.mongo.generic.mongoEnum(Color)) - - val mongo: MongoFormat[Color.Value] = io.sphere.mongo.generic.deriveMongoFormat[Color.Value] - - + val mongo: MongoFormat[Color.Value] = io.sphere.mongo.generic.deriveMongoFormat // mongo java driver knows how to encode/decode Strings val serializedObject = mongo.toMongoValue(Color.Red).asInstanceOf[String] diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/TempUtilMacros.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/TempUtilMacros.scala new file mode 100644 index 00000000..ab8bdd8d --- /dev/null +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/TempUtilMacros.scala @@ -0,0 +1,15 @@ +package io.sphere.mongo.generic + +import scala.quoted.* + +def inspectCode(x: Expr[Any])(using Quotes): Expr[Any] = + // val qq = summon[Quotes] + // import qq.reflect.* + import quotes.reflect.* + + val tree: Tree = x.asTerm + + println(s"----- ${tree.show(using Printer.TreeStructure)}") + x + +inline def inspect(inline x: Any): Any = ${ inspectCode('x) } \ No newline at end of file diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/Test.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/Test.scala deleted file mode 100644 index eb30a764..00000000 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/Test.scala +++ /dev/null @@ -1,14 +0,0 @@ -package io.sphere.mongo.generic - -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class Test extends AnyWordSpec with Matchers: - case class A(x: Int) - - "asd" must { - inspect(A(324535)) - () - } - -end Test \ No newline at end of file From 77787a92b6f149586a70bd44c8c4e53d68214c60 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 22 Feb 2024 15:16:48 +0100 Subject: [PATCH 006/142] some macro test code2 --- .../src/main/scala/io/sphere/mongo/generic/generic.scala | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala index 49966d43..25884822 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala @@ -6,13 +6,15 @@ import scala.quoted.* inline def deriveMongoFormat[A]: MongoFormat[A] = ${ deriveMongoFormatImpl } -def deriveMongoFormatImpl[A](using Type[A], Quotes): Expr[MongoFormat[A]] = { +def deriveMongoFormatImpl[A](using Type[A], Quotes): Expr[MongoFormat[A]] = val q = summon[Quotes] import q.reflect.* val typeRepr = TypeRepr.of[A] if(typeRepr <:< TypeRepr.of[Enumeration#Value]) then - val TypeRef(enumTerm @ TermRef(_, _), _) = typeRepr + val enumTerm = typeRepr match + case TypeRef(tr: TermRef, _) => tr + case _ => report.errorAndAbort("no Enumeration found") val mongoEnumCall = Apply( Ref(Symbol.requiredMethod("io.sphere.mongo.generic.mongoEnum")), @@ -22,9 +24,8 @@ def deriveMongoFormatImpl[A](using Type[A], Quotes): Expr[MongoFormat[A]] = { else println(".......") '{ dummyFormat[A] } -} +end deriveMongoFormatImpl -//Apply(Select(Select(Select(Select(Select(Ident("io"), "sphere"), "mongo"), "generic"), "generic$package"), "mongoEnum"), List(Ident("Color"))) def dummyFormat[A]: MongoFormat[A] = new MongoFormat[A]: override def toMongoValue(a: A): Any = ??? From 33049ff08cd90a1372352c7953fd45bd314ad068 Mon Sep 17 00:00:00 2001 From: Peter Empen Date: Fri, 23 Feb 2024 17:10:45 +0100 Subject: [PATCH 007/142] start module and case-class derivation --- build.sbt | 6 +- .../io/sphere/mongo/generic/generic.scala | 56 ++++++++++++++++--- .../io/sphere/mongo/SerializationTest.scala | 36 ++++++------ .../mongo/generic/MongoFormatMacros.scala | 2 +- 4 files changed, 74 insertions(+), 26 deletions(-) diff --git a/build.sbt b/build.sbt index e01fac33..bc202a6e 100644 --- a/build.sbt +++ b/build.sbt @@ -68,7 +68,11 @@ lazy val standardSettings = Defaults.coreDefaultSettings ++ Seq( lazy val `sphere-libs` = project .in(file(".")) .settings(standardSettings: _*) - .settings(publishArtifact := false, publish := {}) + .settings( + crossScalaVersions := Nil, + publishArtifact := false, + publish := {} + ) .aggregate( `sphere-util`, `sphere-json`, diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala index 25884822..75b2dadc 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala @@ -10,30 +10,70 @@ def deriveMongoFormatImpl[A](using Type[A], Quotes): Expr[MongoFormat[A]] = val q = summon[Quotes] import q.reflect.* val typeRepr = TypeRepr.of[A] + val symbol = TypeTree.of[A].symbol - if(typeRepr <:< TypeRepr.of[Enumeration#Value]) then + if typeRepr <:< TypeRepr.of[Enumeration#Value] then val enumTerm = typeRepr match case TypeRef(tr: TermRef, _) => tr case _ => report.errorAndAbort("no Enumeration found") - - val mongoEnumCall = Apply( + Apply( Ref(Symbol.requiredMethod("io.sphere.mongo.generic.mongoEnum")), - List(Ident(enumTerm)) - ) - mongoEnumCall.asExprOf[MongoFormat[A]] + Ident(enumTerm) :: Nil + ).asExprOf[MongoFormat[A]] + + else if symbol.flags.is(Flags.Case) && symbol.flags.is(Flags.Module) then + // not sure if this case is ever used, at least it's not tested in this library + val moduleName = typeRepr match { + case TermRef(_, module) => module + case _ => report.errorAndAbort("type does not refer to a stable value") + } + Apply( + TypeApply( + Ref(Symbol.requiredMethod("io.sphere.mongo.generic.mongoProduct0")), + TypeIdent(typeRepr.typeSymbol) :: Nil + ), + Ident(TermRef(typeRepr, moduleName)) :: Nil + ).asExprOf[MongoFormat[A]] + + else if (symbol.flags.is(Flags.Case)) + println(s"Hello Case! $symbol") + '{ dummyFormat[A] } + + else if (symbol.flags.is(Flags.Module)) + // has been unused with an implementation that called the non-existing function "mongoSingleton" + report.errorAndAbort("MongoFormat for stand-alone modules is not supported.") + else println(".......") '{ dummyFormat[A] } -end deriveMongoFormatImpl +end deriveMongoFormatImpl def dummyFormat[A]: MongoFormat[A] = new MongoFormat[A]: override def toMongoValue(a: A): Any = ??? override def fromMongoValue(any: Any): A = ??? - def mongoEnum(e: Enumeration): MongoFormat[e.Value] = new MongoFormat[e.Value] { def toMongoValue(a: e.Value): Any = a.toString def fromMongoValue(any: Any): e.Value = e.withName(any.asInstanceOf[String]) } +def mongoProduct0[T <: Product](singleton: T): MongoFormat[T] = ??? /*{ + val (typeField, typeValue) = mongoProduct0Type(singleton) + new MongoFormat[T] { + override def toMongoValue(a: T): Any = { + val dbo = new BasicDBObject() + dbo.append(typeField, typeValue) + dbo + } + + override def fromMongoValue(any: Any): T = any match { + case o: BSONObject => findTypeValue(o, typeField) match { + case Some(t) if t == typeValue => singleton + case Some(t) => sys.error("Invalid type value '" + t + "'. Excepted '%s'".format(typeValue)) + case None => sys.error("Missing type field.") + } + case _ => sys.error("DB object excepted.") + } + } +}*/ diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala index bb1df326..ee075d78 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala @@ -3,31 +3,37 @@ package io.sphere.mongo import com.mongodb.{BasicDBObject, DBObject} import io.sphere.mongo.format.DefaultMongoFormats.* import io.sphere.mongo.format.MongoFormat +import io.sphere.mongo.generic.inspect import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec -object SerializationTest { +object SerializationTest: case class Something(a: Option[Int], b: Int = 2) - object Color extends Enumeration { + object Color extends Enumeration: val Blue, Red, Yellow = Value - } -} -class SerializationTest extends AnyWordSpec with Matchers { + sealed trait PictureSize + case object Small extends PictureSize + case object Medium extends PictureSize + +class SerializationTest extends AnyWordSpec with Matchers: import SerializationTest.* -// "mongoProduct" must { -// "deserialize mongo object" in { -// val dbo = new BasicDBObject() -// dbo.put("a", Integer.valueOf(3)) -// dbo.put("b", Integer.valueOf(4)) -// + "mongoProduct" must { + "deserialize mongo object" in { + val dbo = new BasicDBObject + dbo.put("a", Integer.valueOf(3)) + dbo.put("b", Integer.valueOf(4)) + + val med: MongoFormat[Medium.type] = io.sphere.mongo.generic.deriveMongoFormat + // val mongoFormat: MongoFormat[Something] = io.sphere.mongo.generic.deriveMongoFormat // val something = mongoFormat.fromMongoValue(dbo) -// something must be(Something(Some(3), 4)) -// } -// +// something mustBe Something(Some(3), 4) + } + } + // "generate a format that serializes optional fields with value None as BSON objects without that field" in { // val testFormat: MongoFormat[Something] = // io.sphere.mongo.generic.mongoProduct[Something, Option[Int], Int] { @@ -61,5 +67,3 @@ class SerializationTest extends AnyWordSpec with Matchers { enumValue must be(Color.Red) } } - -} diff --git a/mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/MongoFormatMacros.scala b/mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/MongoFormatMacros.scala index f5c2d34b..b6f343d3 100644 --- a/mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/MongoFormatMacros.scala +++ b/mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/MongoFormatMacros.scala @@ -28,7 +28,7 @@ private[generic] object MongoFormatMacros { } else Set.empty } else Set.empty - def mongoFormatProductApply(c: blackbox.Context)( + private def mongoFormatProductApply(c: blackbox.Context)( tpe: c.universe.Type, classSym: c.universe.ClassSymbol): c.universe.Tree = { import c.universe._ From 4cee24d8600e7a1127ff5f32e54023724edfd37e Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Mon, 26 Feb 2024 10:08:03 +0100 Subject: [PATCH 008/142] Autoderivation example with fake bson --- .../io/sphere/mongo/generic/Derivation.scala | 133 ++++++++++++++++++ .../io/sphere/mongo/generic/generic.scala | 5 +- .../io/sphere/mongo/DerivationSpec.scala | 40 ++++++ 3 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala create mode 100644 mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala new file mode 100644 index 00000000..5f82e3b1 --- /dev/null +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala @@ -0,0 +1,133 @@ +//package io.sphere.mongo.generic +// +//import io.sphere.mongo.format.MongoFormat +// +//import scala.deriving.Mirror +// +//inline given derived[A](using Mirror.Of[A]): MongoFormat[A] = Derivation.derived +// +//private object Derivation: +// inline def derived[A](using m: Mirror.Of[A]): MongoFormat[A] = +// inline m match +// case s: Mirror.SumOf[A] => deriveSum(s) +// case p: Mirror.ProductOf[A] => deriveProduct(p) +// +// inline def deriveSum[A](s: Mirror.SumOf[A]): MongoFormat[A] = dummyFormat[A] +// +// inline def deriveProduct[A](p: Mirror.ProductOf[A]): MongoFormat[A] = new MongoFormat[A]: +// val instances = summonAll[p.MirroredElemTypes].iterator +// override def toMongoValue(a: A): Any = +// val fields = a.asInstanceOf[Product].productIterator +// val fieldNames = a.asInstanceOf[Product].productElementNames +// println(s"-- ${instances.zip(fields).zip(fieldNames)}") +// ??? +// +// override def fromMongoValue(any: Any): A = +// p.fromTuple() +// ??? +// +// +// inline def summonAll[T <: Tuple]: Vector[MongoFormat[Any]] = +// import scala.compiletime.{erasedValue, summonInline} +// inline erasedValue[T] match +// case _: EmptyTuple => Vector.empty +// case _: (t *: ts) => summonInline[MongoFormat[t]].asInstanceOf[MongoFormat[Any]] +: summonAll[ts] +// + +package io.sphere.mongo.generic + +import scala.deriving.Mirror + +type FakeBson = Map[String, Any] | SingleValue | String + +trait FakeMongoFormat[A]: + def toFakeBson(a: A): FakeBson + def fromFakeBson(bson: FakeBson): A + +case class SingleValue(value: Any) + +object FakeMongoFormat: + inline def apply[A: FakeMongoFormat]: FakeMongoFormat[A] = summon + + inline given derive[A](using Mirror.Of[A]): FakeMongoFormat[A] = Derivation.derived + + given FakeMongoFormat[Int] = new FakeMongoFormat[Int]: + override def toFakeBson(a: Int): FakeBson = SingleValue(a) + override def fromFakeBson(bson: FakeBson): Int = + bson match + case SingleValue(int: Int) => int + case _ => throw new Exception("not an int") + + given FakeMongoFormat[String] = new FakeMongoFormat[String]: + override def toFakeBson(a: String): FakeBson = SingleValue(a) + override def fromFakeBson(bson: FakeBson): String = + bson match + case SingleValue(str: String) => str + case _ => throw new Exception("not a String") + + given FakeMongoFormat[Boolean] = new FakeMongoFormat[Boolean]: + override def toFakeBson(a: Boolean): FakeBson = SingleValue(a) + override def fromFakeBson(bson: FakeBson): Boolean = + bson match + case SingleValue(bool: Boolean) => bool + case _ => throw new Exception("not a Boolean") + + private object Derivation: + import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} + + val typeField = "typeDiscriminator" + inline def derived[A](using m: Mirror.Of[A]): FakeMongoFormat[A] = + inline m match + case s: Mirror.SumOf[A] => deriveSum(s) + case p: Mirror.ProductOf[A] => deriveProduct(p) + + inline def deriveSum[A](mirrorOfSum: Mirror.SumOf[A]): FakeMongoFormat[A] = new FakeMongoFormat[A]: + val formatters = summonFormatters[mirrorOfSum.MirroredElemTypes] + val names = constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector.asInstanceOf[Vector[String]] + val formattersByTypeName = names.zip(formatters).toMap + + println(s"sum names $names") + + override def toFakeBson(a: A): FakeBson = + // we never get a trait here, only classes + val typeName = a.asInstanceOf[Product].productPrefix + val map = formattersByTypeName(typeName).toFakeBson(a).asInstanceOf[Map[String, Any]] + map + (typeField -> typeName) + + override def fromFakeBson(bson: FakeBson): A = + bson match + case map: Map[String, _] => + val typeName = map(typeField).asInstanceOf[String] + formattersByTypeName(typeName).fromFakeBson(map).asInstanceOf[A] + case _ => throw new Exception("not a Map") + + end deriveSum + + inline def deriveProduct[A](mirrorOfProduct: Mirror.ProductOf[A]): FakeMongoFormat[A] = new FakeMongoFormat[A]: + val formatters = summonFormatters[mirrorOfProduct.MirroredElemTypes] + val fieldNames = constValueTuple[mirrorOfProduct.MirroredElemLabels].productIterator.toVector.asInstanceOf[Vector[String]] + + override def toFakeBson(a: A): FakeBson = + val values = a.asInstanceOf[Product].productIterator + formatters.zip(values).zip(fieldNames).map { + case ((format, value), fieldName) => + fieldName -> format.toFakeBson(value) + }.toMap + + override def fromFakeBson(bson: FakeBson): A = + bson match + case map: Map[String, _] @unchecked => + val res = fieldNames.zip(formatters).map((fn, format) => format.fromFakeBson(map(fn).asInstanceOf[FakeBson])) + val tuple = Tuple.fromArray(res.toArray) + mirrorOfProduct.fromTuple(tuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) + + case _ => throw new Exception("not a Map") + end deriveProduct + + inline private def summonFormatters[T <: Tuple]: Vector[FakeMongoFormat[Any]] = + inline erasedValue[T] match + case _: EmptyTuple => Vector.empty + case _: (t *: ts) => summonInline[FakeMongoFormat[t]].asInstanceOf[FakeMongoFormat[Any]] +: summonFormatters[ts] + + + diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala index 75b2dadc..f89a3a80 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala @@ -6,8 +6,7 @@ import scala.quoted.* inline def deriveMongoFormat[A]: MongoFormat[A] = ${ deriveMongoFormatImpl } -def deriveMongoFormatImpl[A](using Type[A], Quotes): Expr[MongoFormat[A]] = - val q = summon[Quotes] +def deriveMongoFormatImpl[A](using tpe: Type[A], q: Quotes): Expr[MongoFormat[A]] = import q.reflect.* val typeRepr = TypeRepr.of[A] val symbol = TypeTree.of[A].symbol @@ -27,7 +26,7 @@ def deriveMongoFormatImpl[A](using Type[A], Quotes): Expr[MongoFormat[A]] = case TermRef(_, module) => module case _ => report.errorAndAbort("type does not refer to a stable value") } - Apply( + Apply( TypeApply( Ref(Symbol.requiredMethod("io.sphere.mongo.generic.mongoProduct0")), TypeIdent(typeRepr.typeSymbol) :: Nil diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala new file mode 100644 index 00000000..a3e7db59 --- /dev/null +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala @@ -0,0 +1,40 @@ +package io.sphere.mongo + +import io.sphere.mongo.generic.{FakeBson, FakeMongoFormat, SingleValue} +import io.sphere.mongo.generic.FakeMongoFormat.* +import org.scalatest.wordspec.AnyWordSpec +import org.scalatest.matchers.must.Matchers + +class DerivationSpec extends AnyWordSpec with Matchers: + + "asdasd" in { + + case class Second(xx: Int) + case class TopLvlClass(x: Int, str: String, asd: Second) + + val b = FakeMongoFormat.apply[TopLvlClass] + + val a = TopLvlClass(2234, "aasdasdsd", Second(234)) + val res = b.toFakeBson(a) + + val res2 = b.fromFakeBson(res) + + println(res2) + println(res2 == a) + println(res) + + sealed trait SealedTrait1 + case object Case1 extends SealedTrait1 + case object Case2 extends SealedTrait1 + case class Case3(x: Int) extends SealedTrait1 + + val format2 = FakeMongoFormat[SealedTrait1] + + val bson1 = format2.toFakeBson(Case2) + + println(bson1) + + val sum1 = format2.fromFakeBson(bson1) + + println(sum1) + } \ No newline at end of file From 3a120ca69fd4d0948eb0264f7ad720865f99bc38 Mon Sep 17 00:00:00 2001 From: Peter Empen Date: Thu, 29 Feb 2024 13:25:21 +0100 Subject: [PATCH 009/142] brush up specs --- build.sbt | 2 +- .../io/sphere/mongo/generic/Derivation.scala | 6 +- .../io/sphere/mongo/DerivationSpec.scala | 69 ++++++++++--------- .../io/sphere/mongo/SerializationTest.scala | 3 +- 4 files changed, 44 insertions(+), 36 deletions(-) diff --git a/build.sbt b/build.sbt index bc202a6e..57413a57 100644 --- a/build.sbt +++ b/build.sbt @@ -6,7 +6,7 @@ lazy val scala3 = "3.3.2" // sbt-github-actions needs configuration in `ThisBuild` ThisBuild / crossScalaVersions := Seq(scala2_12, scala2_13, scala3) -ThisBuild / scalaVersion := crossScalaVersions.value.last +ThisBuild / scalaVersion := scala3 ThisBuild / githubWorkflowPublishTargetBranches := List() ThisBuild / githubWorkflowJavaVersions := List(JavaSpec.temurin("17")) ThisBuild / githubWorkflowBuildPreamble ++= List( diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala index 5f82e3b1..0a2b2df6 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala @@ -75,18 +75,18 @@ object FakeMongoFormat: private object Derivation: import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} - val typeField = "typeDiscriminator" inline def derived[A](using m: Mirror.Of[A]): FakeMongoFormat[A] = inline m match case s: Mirror.SumOf[A] => deriveSum(s) case p: Mirror.ProductOf[A] => deriveProduct(p) - inline def deriveSum[A](mirrorOfSum: Mirror.SumOf[A]): FakeMongoFormat[A] = new FakeMongoFormat[A]: + inline private def deriveSum[A](mirrorOfSum: Mirror.SumOf[A]): FakeMongoFormat[A] = new FakeMongoFormat[A]: + val typeField = "typeDiscriminator" val formatters = summonFormatters[mirrorOfSum.MirroredElemTypes] val names = constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector.asInstanceOf[Vector[String]] val formattersByTypeName = names.zip(formatters).toMap - println(s"sum names $names") + // println(s"sum names $names") override def toFakeBson(a: A): FakeBson = // we never get a trait here, only classes diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala index a3e7db59..0235a347 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala @@ -7,34 +7,41 @@ import org.scalatest.matchers.must.Matchers class DerivationSpec extends AnyWordSpec with Matchers: - "asdasd" in { - - case class Second(xx: Int) - case class TopLvlClass(x: Int, str: String, asd: Second) - - val b = FakeMongoFormat.apply[TopLvlClass] - - val a = TopLvlClass(2234, "aasdasdsd", Second(234)) - val res = b.toFakeBson(a) - - val res2 = b.fromFakeBson(res) - - println(res2) - println(res2 == a) - println(res) - - sealed trait SealedTrait1 - case object Case1 extends SealedTrait1 - case object Case2 extends SealedTrait1 - case class Case3(x: Int) extends SealedTrait1 - - val format2 = FakeMongoFormat[SealedTrait1] - - val bson1 = format2.toFakeBson(Case2) - - println(bson1) - - val sum1 = format2.fromFakeBson(bson1) - - println(sum1) - } \ No newline at end of file + "MongoFormat derivation" should { + "support composition" in { + case class Container(i: Int, str: String, component: Component) + case class Component(i: Int) + + val format = FakeMongoFormat[Container] + + val container = Container(123, "anything", Component(456)) + val bson = format.toFakeBson(container) + val roundtrip = format.fromFakeBson(bson) + + // println(bson) + // println(roundtrip) + roundtrip mustBe container + } + + "support ADT" in { + sealed trait Root + case object Object1 extends Root + case object Object2 extends Root + case class Class(i: Int) extends Root + + val format = FakeMongoFormat[Root] + + def roundtrip(member: Root): Unit = + val bson = format.toFakeBson(member) + val roundtrip = format.fromFakeBson(bson) + + // println(member) + // println(bson) + // println(roundtrip) + roundtrip mustBe member + + roundtrip(Object1) + roundtrip(Object2) + roundtrip(Class(0)) + } + } diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala index ee075d78..9ef126b2 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala @@ -26,7 +26,8 @@ class SerializationTest extends AnyWordSpec with Matchers: dbo.put("a", Integer.valueOf(3)) dbo.put("b", Integer.valueOf(4)) - val med: MongoFormat[Medium.type] = io.sphere.mongo.generic.deriveMongoFormat + // TODO + // val med: MongoFormat[Medium.type] = io.sphere.mongo.generic.deriveMongoFormat // val mongoFormat: MongoFormat[Something] = io.sphere.mongo.generic.deriveMongoFormat // val something = mongoFormat.fromMongoValue(dbo) From c888f4f060deb85cdfa56a3c8f892d5dfed1e57e Mon Sep 17 00:00:00 2001 From: Peter Empen Date: Thu, 29 Feb 2024 13:31:37 +0100 Subject: [PATCH 010/142] add .bsp to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d8ece97f..f724f421 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ boot tags lib_managed *.iml +.bsp .idea /.sbtconfig /tmtags From cf41666794ba2f5cea6ae9d151f82a1a53febab2 Mon Sep 17 00:00:00 2001 From: Peter Empen Date: Thu, 29 Feb 2024 13:48:52 +0100 Subject: [PATCH 011/142] scalafmt --- util/src/test/scala/HighPrecisionMoneySpec.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/util/src/test/scala/HighPrecisionMoneySpec.scala b/util/src/test/scala/HighPrecisionMoneySpec.scala index 68493174..6854c0b5 100644 --- a/util/src/test/scala/HighPrecisionMoneySpec.scala +++ b/util/src/test/scala/HighPrecisionMoneySpec.scala @@ -15,7 +15,8 @@ class HighPrecisionMoneySpec extends AnyFunSpec with Matchers with ScalaCheckDri import HighPrecisionMoney.ImplicitsString._ import HighPrecisionMoney.ImplicitsStringPrecise._ - implicit val defaultRoundingMode: BigDecimal.RoundingMode.Value = BigDecimal.RoundingMode.HALF_EVEN + implicit val defaultRoundingMode: BigDecimal.RoundingMode.Value = + BigDecimal.RoundingMode.HALF_EVEN val Euro: Currency = Currency.getInstance("EUR") From 4da664ca14f32ba62c309a2873c676d7aabba68b Mon Sep 17 00:00:00 2001 From: Peter Empen Date: Fri, 1 Mar 2024 10:02:49 +0100 Subject: [PATCH 012/142] fix SBT warnings on target --- build.sbt | 4 ++-- .../io/sphere/mongo/generic/TempUtilMacros.scala | 15 --------------- 2 files changed, 2 insertions(+), 17 deletions(-) delete mode 100644 mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/TempUtilMacros.scala diff --git a/build.sbt b/build.sbt index 57413a57..154ecc32 100644 --- a/build.sbt +++ b/build.sbt @@ -49,8 +49,8 @@ lazy val standardSettings = Defaults.coreDefaultSettings ++ Seq( javacOptions ++= Seq("-deprecation", "-Xlint:unchecked"), // targets Java 8 bytecode (scalac & javac) ThisBuild / scalacOptions ++= { - if (scalaVersion.value.startsWith("2.12")) Seq.empty - else Seq("-target", "8") + if (scalaVersion.value.startsWith("2.13")) Seq("-release", "8") + else Nil }, ThisBuild / javacOptions ++= Seq("-source", "8", "-target", "8"), Test / testOptions += Tests.Argument(TestFrameworks.ScalaTest, "-oDF"), diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/TempUtilMacros.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/TempUtilMacros.scala deleted file mode 100644 index ab8bdd8d..00000000 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/TempUtilMacros.scala +++ /dev/null @@ -1,15 +0,0 @@ -package io.sphere.mongo.generic - -import scala.quoted.* - -def inspectCode(x: Expr[Any])(using Quotes): Expr[Any] = - // val qq = summon[Quotes] - // import qq.reflect.* - import quotes.reflect.* - - val tree: Tree = x.asTerm - - println(s"----- ${tree.show(using Printer.TreeStructure)}") - x - -inline def inspect(inline x: Any): Any = ${ inspectCode('x) } \ No newline at end of file From a8845d149be5896567128a53d711c8541e332405 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 19 Apr 2024 17:25:11 +0200 Subject: [PATCH 013/142] Port the initial implementation from FakeBson to Actual MongoDB types --- .scalafmt.conf | 2 +- .../io/sphere/mongo/generic/Derivation.scala | 198 +++++++----------- .../io/sphere/mongo/DerivationSpec.scala | 16 +- .../io/sphere/mongo/SerializationTest.scala | 5 +- 4 files changed, 92 insertions(+), 129 deletions(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index 171bbb39..ec9f8145 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,6 +1,6 @@ version = 3.7.17 -runner.dialect = scala213 +runner.dialect = scala3 maxColumn = 100 diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala index 0a2b2df6..83696b1e 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala @@ -1,133 +1,97 @@ -//package io.sphere.mongo.generic -// -//import io.sphere.mongo.format.MongoFormat -// -//import scala.deriving.Mirror -// -//inline given derived[A](using Mirror.Of[A]): MongoFormat[A] = Derivation.derived -// -//private object Derivation: -// inline def derived[A](using m: Mirror.Of[A]): MongoFormat[A] = -// inline m match -// case s: Mirror.SumOf[A] => deriveSum(s) -// case p: Mirror.ProductOf[A] => deriveProduct(p) -// -// inline def deriveSum[A](s: Mirror.SumOf[A]): MongoFormat[A] = dummyFormat[A] -// -// inline def deriveProduct[A](p: Mirror.ProductOf[A]): MongoFormat[A] = new MongoFormat[A]: -// val instances = summonAll[p.MirroredElemTypes].iterator -// override def toMongoValue(a: A): Any = -// val fields = a.asInstanceOf[Product].productIterator -// val fieldNames = a.asInstanceOf[Product].productElementNames -// println(s"-- ${instances.zip(fields).zip(fieldNames)}") -// ??? -// -// override def fromMongoValue(any: Any): A = -// p.fromTuple() -// ??? -// -// -// inline def summonAll[T <: Tuple]: Vector[MongoFormat[Any]] = -// import scala.compiletime.{erasedValue, summonInline} -// inline erasedValue[T] match -// case _: EmptyTuple => Vector.empty -// case _: (t *: ts) => summonInline[MongoFormat[t]].asInstanceOf[MongoFormat[Any]] +: summonAll[ts] -// - package io.sphere.mongo.generic -import scala.deriving.Mirror - -type FakeBson = Map[String, Any] | SingleValue | String +import com.mongodb.BasicDBObject +import org.bson.types.ObjectId -trait FakeMongoFormat[A]: - def toFakeBson(a: A): FakeBson - def fromFakeBson(bson: FakeBson): A +import java.util.UUID +import java.util.regex.Pattern +import scala.deriving.Mirror -case class SingleValue(value: Any) +type SimpleMongoType = UUID | String | ObjectId | Short | Int | Long | Float | Double | Boolean | + Pattern +type MongoType = BasicDBObject | SimpleMongoType -object FakeMongoFormat: - inline def apply[A: FakeMongoFormat]: FakeMongoFormat[A] = summon +trait TypedMongoFormat[A] extends Serializable { + def toMongoValue(a: A): MongoType + def fromMongoValue(mongoType: MongoType): A +} - inline given derive[A](using Mirror.Of[A]): FakeMongoFormat[A] = Derivation.derived +private final class NativeMongoFormat[A <: SimpleMongoType] extends TypedMongoFormat[A] { + def toMongoValue(a: A): MongoType = a + def fromMongoValue(any: MongoType): A = any.asInstanceOf[A] +} - given FakeMongoFormat[Int] = new FakeMongoFormat[Int]: - override def toFakeBson(a: Int): FakeBson = SingleValue(a) - override def fromFakeBson(bson: FakeBson): Int = - bson match - case SingleValue(int: Int) => int - case _ => throw new Exception("not an int") +object TypedMongoFormat: + inline def apply[A: TypedMongoFormat]: TypedMongoFormat[A] = summon - given FakeMongoFormat[String] = new FakeMongoFormat[String]: - override def toFakeBson(a: String): FakeBson = SingleValue(a) - override def fromFakeBson(bson: FakeBson): String = - bson match - case SingleValue(str: String) => str - case _ => throw new Exception("not a String") + inline given derive[A](using Mirror.Of[A]): TypedMongoFormat[A] = Derivation.derived - given FakeMongoFormat[Boolean] = new FakeMongoFormat[Boolean]: - override def toFakeBson(a: Boolean): FakeBson = SingleValue(a) - override def fromFakeBson(bson: FakeBson): Boolean = - bson match - case SingleValue(bool: Boolean) => bool - case _ => throw new Exception("not a Boolean") + given TypedMongoFormat[Int] = new NativeMongoFormat[Int] + given TypedMongoFormat[String] = new NativeMongoFormat[String] + given TypedMongoFormat[Boolean] = new NativeMongoFormat[Boolean] private object Derivation: import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} - inline def derived[A](using m: Mirror.Of[A]): FakeMongoFormat[A] = + inline def derived[A](using m: Mirror.Of[A]): TypedMongoFormat[A] = inline m match - case s: Mirror.SumOf[A] => deriveSum(s) - case p: Mirror.ProductOf[A] => deriveProduct(p) - - inline private def deriveSum[A](mirrorOfSum: Mirror.SumOf[A]): FakeMongoFormat[A] = new FakeMongoFormat[A]: - val typeField = "typeDiscriminator" - val formatters = summonFormatters[mirrorOfSum.MirroredElemTypes] - val names = constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector.asInstanceOf[Vector[String]] - val formattersByTypeName = names.zip(formatters).toMap - - // println(s"sum names $names") - - override def toFakeBson(a: A): FakeBson = - // we never get a trait here, only classes - val typeName = a.asInstanceOf[Product].productPrefix - val map = formattersByTypeName(typeName).toFakeBson(a).asInstanceOf[Map[String, Any]] - map + (typeField -> typeName) - - override def fromFakeBson(bson: FakeBson): A = - bson match - case map: Map[String, _] => - val typeName = map(typeField).asInstanceOf[String] - formattersByTypeName(typeName).fromFakeBson(map).asInstanceOf[A] - case _ => throw new Exception("not a Map") - - end deriveSum - - inline def deriveProduct[A](mirrorOfProduct: Mirror.ProductOf[A]): FakeMongoFormat[A] = new FakeMongoFormat[A]: - val formatters = summonFormatters[mirrorOfProduct.MirroredElemTypes] - val fieldNames = constValueTuple[mirrorOfProduct.MirroredElemLabels].productIterator.toVector.asInstanceOf[Vector[String]] - - override def toFakeBson(a: A): FakeBson = - val values = a.asInstanceOf[Product].productIterator - formatters.zip(values).zip(fieldNames).map { - case ((format, value), fieldName) => - fieldName -> format.toFakeBson(value) - }.toMap - - override def fromFakeBson(bson: FakeBson): A = - bson match - case map: Map[String, _] @unchecked => - val res = fieldNames.zip(formatters).map((fn, format) => format.fromFakeBson(map(fn).asInstanceOf[FakeBson])) - val tuple = Tuple.fromArray(res.toArray) - mirrorOfProduct.fromTuple(tuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) - - case _ => throw new Exception("not a Map") - end deriveProduct - - inline private def summonFormatters[T <: Tuple]: Vector[FakeMongoFormat[Any]] = + case s: Mirror.SumOf[A] => deriveTrait(s) + case p: Mirror.ProductOf[A] => deriveCaseClass(p) + + inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): TypedMongoFormat[A] = + new TypedMongoFormat[A]: + val typeField = "typeDiscriminator" + val formatters = summonFormatters[mirrorOfSum.MirroredElemTypes] + val names = constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector + .asInstanceOf[Vector[String]] + val formattersByTypeName = names.zip(formatters).toMap + + override def toMongoValue(a: A): MongoType = + // we never get a trait here, only classes, it's safe to assume Product + val typeName = a.asInstanceOf[Product].productPrefix + val bson = formattersByTypeName(typeName).toMongoValue(a).asInstanceOf[BasicDBObject] + bson.put(typeField, typeName) + bson + + override def fromMongoValue(bson: MongoType): A = + bson match + case bson: BasicDBObject => + val typeName = bson.get(typeField).asInstanceOf[String] + formattersByTypeName(typeName).fromMongoValue(bson).asInstanceOf[A] + case _ => throw new Exception("idk yet") + end deriveTrait + + inline def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): TypedMongoFormat[A] = + new TypedMongoFormat[A]: + val formatters = summonFormatters[mirrorOfProduct.MirroredElemTypes] + val fieldNames = + constValueTuple[mirrorOfProduct.MirroredElemLabels].productIterator.toVector + .asInstanceOf[Vector[String]] + + override def toMongoValue(a: A): MongoType = + val bson = new BasicDBObject() + val values = a.asInstanceOf[Product].productIterator + formatters.zip(values).zip(fieldNames).foreach { case ((format, value), fieldName) => + bson.put(fieldName, format.toMongoValue(value)) + } + bson + + override def fromMongoValue(mongoType: MongoType): A = + mongoType match + case bson: BasicDBObject => + val fieldsAsAList = fieldNames + .zip(formatters) + .map((fieldName, format) => + format.fromMongoValue(bson.get(fieldName).asInstanceOf[MongoType])) + val tuple = Tuple.fromArray(fieldsAsAList.toArray) + mirrorOfProduct.fromTuple(tuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) + + case _ => throw new Exception("not a Map") + end deriveCaseClass + + inline private def summonFormatters[T <: Tuple]: Vector[TypedMongoFormat[Any]] = inline erasedValue[T] match case _: EmptyTuple => Vector.empty - case _: (t *: ts) => summonInline[FakeMongoFormat[t]].asInstanceOf[FakeMongoFormat[Any]] +: summonFormatters[ts] - - - + case _: (t *: ts) => + summonInline[TypedMongoFormat[t]] + .asInstanceOf[TypedMongoFormat[Any]] +: summonFormatters[ts] diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala index 0235a347..df287460 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala @@ -1,7 +1,7 @@ package io.sphere.mongo -import io.sphere.mongo.generic.{FakeBson, FakeMongoFormat, SingleValue} -import io.sphere.mongo.generic.FakeMongoFormat.* +import io.sphere.mongo.generic.TypedMongoFormat +import io.sphere.mongo.generic.TypedMongoFormat.* import org.scalatest.wordspec.AnyWordSpec import org.scalatest.matchers.must.Matchers @@ -12,11 +12,11 @@ class DerivationSpec extends AnyWordSpec with Matchers: case class Container(i: Int, str: String, component: Component) case class Component(i: Int) - val format = FakeMongoFormat[Container] + val format = TypedMongoFormat[Container] val container = Container(123, "anything", Component(456)) - val bson = format.toFakeBson(container) - val roundtrip = format.fromFakeBson(bson) + val bson = format.toMongoValue(container) + val roundtrip = format.fromMongoValue(bson) // println(bson) // println(roundtrip) @@ -29,11 +29,11 @@ class DerivationSpec extends AnyWordSpec with Matchers: case object Object2 extends Root case class Class(i: Int) extends Root - val format = FakeMongoFormat[Root] + val format = TypedMongoFormat[Root] def roundtrip(member: Root): Unit = - val bson = format.toFakeBson(member) - val roundtrip = format.fromFakeBson(bson) + val bson = format.toMongoValue(member) + val roundtrip = format.fromMongoValue(bson) // println(member) // println(bson) diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala index 9ef126b2..03811da0 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala @@ -3,7 +3,6 @@ package io.sphere.mongo import com.mongodb.{BasicDBObject, DBObject} import io.sphere.mongo.format.DefaultMongoFormats.* import io.sphere.mongo.format.MongoFormat -import io.sphere.mongo.generic.inspect import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -27,8 +26,8 @@ class SerializationTest extends AnyWordSpec with Matchers: dbo.put("b", Integer.valueOf(4)) // TODO - // val med: MongoFormat[Medium.type] = io.sphere.mongo.generic.deriveMongoFormat - +// val med: MongoFormat[Medium.type] = io.sphere.mongo.generic.deriveMongoFormat +//// // val mongoFormat: MongoFormat[Something] = io.sphere.mongo.generic.deriveMongoFormat // val something = mongoFormat.fromMongoValue(dbo) // something mustBe Something(Some(3), 4) From 396417a06b3afd0206ca7a7005f546fc5b268ec5 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Mon, 29 Apr 2024 16:43:13 +0200 Subject: [PATCH 014/142] Add MongoKey and MongoEmbedded implementations --- build.sbt | 2 +- .../mongo/generic/AnnotationReader.scala | 99 ++++++++++++++ .../io/sphere/mongo/generic/Annotations.scala | 11 ++ .../io/sphere/mongo/generic/Derivation.scala | 77 +++++++++-- .../io/sphere/mongo/generic/generic.scala | 2 +- .../src/test/scala/MongoUtils.scala | 10 ++ .../io/sphere/mongo/DerivationSpec.scala | 28 ++-- .../io/sphere/mongo/SerializationTest.scala | 65 +++++---- .../mongo/format/OptionMongoFormatSpec.scala | 96 ++++++++++++++ .../mongo/generic/MongoEmbeddedSpec.scala | 125 ++++++++++++++++++ .../sphere/mongo/generic/MongoKeySpec.scala | 43 ++++++ 11 files changed, 499 insertions(+), 59 deletions(-) create mode 100644 mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala create mode 100644 mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Annotations.scala create mode 100644 mongo/mongo-derivation-scala-3/src/test/scala/MongoUtils.scala create mode 100644 mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala create mode 100644 mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala create mode 100644 mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala diff --git a/build.sbt b/build.sbt index 154ecc32..ae5af1a0 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ import pl.project13.scala.sbt.JmhPlugin lazy val scala2_12 = "2.12.18" lazy val scala2_13 = "2.13.12" -lazy val scala3 = "3.3.2" +lazy val scala3 = "3.4.1" // sbt-github-actions needs configuration in `ThisBuild` ThisBuild / crossScalaVersions := Seq(scala2_12, scala2_13, scala3) diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala new file mode 100644 index 00000000..cac2c2d6 --- /dev/null +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala @@ -0,0 +1,99 @@ +package io.sphere.mongo.generic + +import scala.quoted.{Expr, Quotes, Type, Varargs} + +private type MA = MongoAnnotation + +case class Field(name: String, embedded: Boolean, ignored: Boolean, mongoKey: Option[MongoKey]) { + val fieldName: String = mongoKey.map(_.newFieldName).getOrElse(name) +} +case class Annotations( + name: String, + forType: Vector[MA], + byField: Map[String, Vector[MA]], + fields: Vector[Field] +) + +case class AllAnnotations( + top: Annotations, + subtypes: Map[String, Annotations] +) + +// TODO this can probably be simplifed later +class AnnotationReader(using q: Quotes): + import q.reflect.* + + def readCaseClassMetaData[T: Type]: Expr[Annotations] = { + val sym = TypeRepr.of[T].typeSymbol + topAnnotations(sym) + } + + def allAnnotations[T: Type]: Expr[AllAnnotations] = { + val sym = TypeRepr.of[T].typeSymbol + + '{ + AllAnnotations( + top = ${ topAnnotations(sym) }, + subtypes = ${ subtypeAnnotations(sym) } + ) + } + } + + private def annotationTree(tree: Tree): Option[Expr[MA]] = + Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[MA]).map(_.asExprOf[MA]) + + private def findEmbedded(tree: Tree): Boolean = + Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[MongoEmbedded]).isDefined + + private def findIgnored(tree: Tree): Boolean = + Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[MongoIgnore]).isDefined + + private def findKey(tree: Tree): Option[Expr[MongoKey]] = + Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[MongoKey]).map(_.asExprOf[MongoKey]) + + private def collectFieldInfo(s: Symbol): Expr[Field] = + val embedded = Expr(s.annotations.exists(findEmbedded)) + val ignored = Expr(s.annotations.exists(findIgnored)) + val name = Expr(s.name) + s.annotations.map(findKey).find(_.isDefined).flatten match { + case Some(k) => + '{ Field(name = $name, embedded = $embedded, ignored = $ignored, mongoKey = Some($k)) } + case None => + '{ Field(name = $name, embedded = $embedded, ignored = $ignored, mongoKey = None) } + } + + private def fieldAnnotations(s: Symbol): Expr[(String, Vector[MA])] = + val annots = Varargs(s.annotations.flatMap(annotationTree)) + val name = Expr(s.name) + + '{ $name -> Vector($annots*) } + end fieldAnnotations + + private def topAnnotations(sym: Symbol): Expr[Annotations] = + val topAnns = Varargs(sym.annotations.flatMap(annotationTree)) + val caseParams = sym.primaryConstructor.paramSymss.take(1).flatten + val fieldAnns = Varargs(caseParams.map(fieldAnnotations)) + val fields = Varargs(caseParams.map(collectFieldInfo)) + val name = Expr(sym.name) + + '{ + Annotations( + name = $name, + forType = Vector($topAnns*), + byField = Map($fieldAnns*), + fields = Vector($fields*) + ) + } + end topAnnotations + + private def subtypeAnnotation(sym: Symbol): Expr[(String, Annotations)] = + val name = Expr(sym.name) + val annots = topAnnotations(sym) + '{ ($name, $annots) } + end subtypeAnnotation + + private def subtypeAnnotations(sym: Symbol): Expr[Map[String, Annotations]] = + val subtypes = Varargs(sym.children.map(subtypeAnnotation)) + '{ Map($subtypes*) } + +end AnnotationReader diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Annotations.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Annotations.scala new file mode 100644 index 00000000..5ef08963 --- /dev/null +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Annotations.scala @@ -0,0 +1,11 @@ +package io.sphere.mongo.generic + +import scala.annotation.StaticAnnotation + +sealed trait MongoAnnotation extends StaticAnnotation + +case class MongoEmbedded() extends MongoAnnotation +case class MongoIgnore() extends MongoAnnotation +case class MongoKey(newFieldName: String) extends MongoAnnotation +case class MongoTypeHintField(typeDiscriminator: String) extends MongoAnnotation +case class MongoTypeHint(newClassName: String) extends MongoAnnotation diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala index 83696b1e..77174a95 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala @@ -1,19 +1,25 @@ package io.sphere.mongo.generic import com.mongodb.BasicDBObject +import org.bson.BSONObject import org.bson.types.ObjectId import java.util.UUID import java.util.regex.Pattern import scala.deriving.Mirror +import scala.quoted.* +object MongoNothing type SimpleMongoType = UUID | String | ObjectId | Short | Int | Long | Float | Double | Boolean | Pattern -type MongoType = BasicDBObject | SimpleMongoType +type MongoType = BasicDBObject | SimpleMongoType | MongoNothing.type trait TypedMongoFormat[A] extends Serializable { def toMongoValue(a: A): MongoType def fromMongoValue(mongoType: MongoType): A + +// /** needed JSON fields - ignored if empty */ + val fieldNames: Vector[String] = TypedMongoFormat.emptyFieldsSet } private final class NativeMongoFormat[A <: SimpleMongoType] extends TypedMongoFormat[A] { @@ -21,8 +27,14 @@ private final class NativeMongoFormat[A <: SimpleMongoType] extends TypedMongoFo def fromMongoValue(any: MongoType): A = any.asInstanceOf[A] } +inline def deriveMongoFormat[A: TypedMongoFormat]: TypedMongoFormat[A] = summon + object TypedMongoFormat: - inline def apply[A: TypedMongoFormat]: TypedMongoFormat[A] = summon + private val emptyFieldsSet: Vector[String] = Vector.empty + inline def readCaseClassMetaData[T]: Annotations = ${ readCaseClassMetaDataImpl[T] } + + private def readCaseClassMetaDataImpl[T: Type](using Quotes): Expr[Annotations] = + AnnotationReader().readCaseClassMetaData[T] inline given derive[A](using Mirror.Of[A]): TypedMongoFormat[A] = Derivation.derived @@ -30,6 +42,33 @@ object TypedMongoFormat: given TypedMongoFormat[String] = new NativeMongoFormat[String] given TypedMongoFormat[Boolean] = new NativeMongoFormat[Boolean] + given [A](using TypedMongoFormat[A]): TypedMongoFormat[Option[A]] = + new TypedMongoFormat[Option[A]]: + override def toMongoValue(a: Option[A]): MongoType = + a match + case Some(value) => summon[TypedMongoFormat[A]].toMongoValue(value) + case None => MongoNothing + + override def fromMongoValue(mongoType: MongoType): Option[A] = + val fieldNames = summon[TypedMongoFormat[A]].fieldNames + if (mongoType == null) None + else + mongoType match + case s: SimpleMongoType => Some(summon[TypedMongoFormat[A]].fromMongoValue(s)) + case bson: BasicDBObject => + val bsonFieldNames = bson.keySet().toArray + if (fieldNames.nonEmpty && bsonFieldNames.intersect(fieldNames).isEmpty) None + else Some(summon[TypedMongoFormat[A]].fromMongoValue(bson)) + case MongoNothing => None // This can't happen, but it makes the compiler happy + + private def addField(bson: BasicDBObject, field: Field, mongoType: MongoType) = + mongoType match + case s: SimpleMongoType => bson.put(field.fieldName, s) + case innerBson: BasicDBObject => + if (field.embedded) innerBson.entrySet().forEach(p => bson.put(p.getKey, p.getValue)) + else bson.put(field.fieldName, innerBson) + case MongoNothing => + private object Derivation: import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} @@ -40,6 +79,7 @@ object TypedMongoFormat: inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): TypedMongoFormat[A] = new TypedMongoFormat[A]: + val annotations = readCaseClassMetaData[A] val typeField = "typeDiscriminator" val formatters = summonFormatters[mirrorOfSum.MirroredElemTypes] val names = constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector @@ -58,35 +98,44 @@ object TypedMongoFormat: case bson: BasicDBObject => val typeName = bson.get(typeField).asInstanceOf[String] formattersByTypeName(typeName).fromMongoValue(bson).asInstanceOf[A] - case _ => throw new Exception("idk yet") + case x => + throw new Exception(s"BsonObject is expected for a Trait subtype, instead got $x") end deriveTrait - inline def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): TypedMongoFormat[A] = + inline private def deriveCaseClass[A]( + mirrorOfProduct: Mirror.ProductOf[A]): TypedMongoFormat[A] = new TypedMongoFormat[A]: + val caseClassMetaData = readCaseClassMetaData[A] val formatters = summonFormatters[mirrorOfProduct.MirroredElemTypes] - val fieldNames = - constValueTuple[mirrorOfProduct.MirroredElemLabels].productIterator.toVector - .asInstanceOf[Vector[String]] + val fieldsAndFormatters = caseClassMetaData.fields.zip(formatters) + + override val fieldNames: Vector[String] = fieldsAndFormatters.flatMap((field, formatter) => + if (field.embedded) formatter.fieldNames :+ field.name + else Vector(field.name)) override def toMongoValue(a: A): MongoType = val bson = new BasicDBObject() val values = a.asInstanceOf[Product].productIterator - formatters.zip(values).zip(fieldNames).foreach { case ((format, value), fieldName) => - bson.put(fieldName, format.toMongoValue(value)) + formatters.zip(values).zip(caseClassMetaData.fields).foreach { + case ((format, value), field) => + addField(bson, field, format.toMongoValue(value)) } bson override def fromMongoValue(mongoType: MongoType): A = mongoType match case bson: BasicDBObject => - val fieldsAsAList = fieldNames - .zip(formatters) - .map((fieldName, format) => - format.fromMongoValue(bson.get(fieldName).asInstanceOf[MongoType])) + val fieldsAsAList = fieldsAndFormatters + .map { (field, format) => + if (field.embedded) + format.fromMongoValue(bson) + else + format.fromMongoValue(bson.get(field.fieldName).asInstanceOf[MongoType]) + } val tuple = Tuple.fromArray(fieldsAsAList.toArray) mirrorOfProduct.fromTuple(tuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) - case _ => throw new Exception("not a Map") + case x => throw new Exception(s"BsonObject is expected for a class, instead got: ${x}") end deriveCaseClass inline private def summonFormatters[T <: Tuple]: Vector[TypedMongoFormat[Any]] = diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala index f89a3a80..2519a6ee 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala @@ -4,7 +4,7 @@ import io.sphere.mongo.format.MongoFormat import scala.quoted.* -inline def deriveMongoFormat[A]: MongoFormat[A] = ${ deriveMongoFormatImpl } +//inline def deriveMongoFormat[A]: MongoFormat[A] = ${ deriveMongoFormatImpl } def deriveMongoFormatImpl[A](using tpe: Type[A], q: Quotes): Expr[MongoFormat[A]] = import q.reflect.* diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/MongoUtils.scala b/mongo/mongo-derivation-scala-3/src/test/scala/MongoUtils.scala new file mode 100644 index 00000000..71c641e8 --- /dev/null +++ b/mongo/mongo-derivation-scala-3/src/test/scala/MongoUtils.scala @@ -0,0 +1,10 @@ +package io.sphere.mongo + +import com.mongodb.BasicDBObject + +object MongoUtils { + + def dbObj(pairs: (String, Any)*) = + pairs.foldLeft(new BasicDBObject) { case (obj, (key, value)) => obj.append(key, value) } + +} diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala index df287460..33c6e50a 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala @@ -1,6 +1,6 @@ -package io.sphere.mongo +package io.sphere.mongo.format -import io.sphere.mongo.generic.TypedMongoFormat +import io.sphere.mongo.generic.{MongoEmbedded, MongoKey, MongoTypeHintField, TypedMongoFormat} import io.sphere.mongo.generic.TypedMongoFormat.* import org.scalatest.wordspec.AnyWordSpec import org.scalatest.matchers.must.Matchers @@ -12,14 +12,12 @@ class DerivationSpec extends AnyWordSpec with Matchers: case class Container(i: Int, str: String, component: Component) case class Component(i: Int) - val format = TypedMongoFormat[Container] + val format = io.sphere.mongo.generic.deriveMongoFormat[Container] val container = Container(123, "anything", Component(456)) val bson = format.toMongoValue(container) val roundtrip = format.fromMongoValue(bson) - // println(bson) - // println(roundtrip) roundtrip mustBe container } @@ -29,19 +27,29 @@ class DerivationSpec extends AnyWordSpec with Matchers: case object Object2 extends Root case class Class(i: Int) extends Root - val format = TypedMongoFormat[Root] + val format = io.sphere.mongo.generic.deriveMongoFormat[Root] def roundtrip(member: Root): Unit = val bson = format.toMongoValue(member) val roundtrip = format.fromMongoValue(bson) - - // println(member) - // println(bson) - // println(roundtrip) roundtrip mustBe member roundtrip(Object1) roundtrip(Object2) roundtrip(Class(0)) + + } + + "annotations" in { + + case class InnerClass(x: String) + @MongoTypeHintField("pictureType") + sealed trait Root + case object Object1 extends Root + case object Object2 extends Root + case class Class(i: Int, @MongoEmbedded inner: InnerClass) extends Root + + val res = readCaseClassMetaData[Root] + } } diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala index 03811da0..3aac6aa7 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala @@ -1,8 +1,7 @@ package io.sphere.mongo -import com.mongodb.{BasicDBObject, DBObject} -import io.sphere.mongo.format.DefaultMongoFormats.* -import io.sphere.mongo.format.MongoFormat +import com.mongodb.BasicDBObject +import io.sphere.mongo.generic.TypedMongoFormat import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -25,45 +24,45 @@ class SerializationTest extends AnyWordSpec with Matchers: dbo.put("a", Integer.valueOf(3)) dbo.put("b", Integer.valueOf(4)) - // TODO -// val med: MongoFormat[Medium.type] = io.sphere.mongo.generic.deriveMongoFormat -//// -// val mongoFormat: MongoFormat[Something] = io.sphere.mongo.generic.deriveMongoFormat -// val something = mongoFormat.fromMongoValue(dbo) -// something mustBe Something(Some(3), 4) + // TODO what is this? :D + val med: TypedMongoFormat[Medium.type] = io.sphere.mongo.generic.deriveMongoFormat + + val mongoFormat: TypedMongoFormat[Something] = io.sphere.mongo.generic.deriveMongoFormat + val something = mongoFormat.fromMongoValue(dbo) + something mustBe Something(Some(3), 4) + } + + "generate a format that serializes optional fields with value None as BSON objects without that field" in { + val testFormat: TypedMongoFormat[Something] = io.sphere.mongo.generic.deriveMongoFormat + + val something = Something(None, 1) + val serializedObject = testFormat.toMongoValue(something).asInstanceOf[BasicDBObject] + serializedObject.keySet().contains("b") must be(true) + serializedObject.keySet().contains("a") must be(false) + + testFormat.fromMongoValue(serializedObject) must be(something) } - } -// "generate a format that serializes optional fields with value None as BSON objects without that field" in { -// val testFormat: MongoFormat[Something] = -// io.sphere.mongo.generic.mongoProduct[Something, Option[Int], Int] { -// (a: Option[Int], b: Int) => Something(a, b) -// } -// -// val serializedObject = testFormat.toMongoValue(Something(None, 1)).asInstanceOf[DBObject] -// serializedObject.keySet().contains("b") must be(true) -// serializedObject.keySet().contains("a") must be(false) -// } -// // "generate a format that use default values" in { +// // TODO https://stackoverflow.com/questions/68421043/type-class-derivation-accessing-default-values // val dbo = new BasicDBObject() // dbo.put("a", Integer.valueOf(3)) // -// val mongoFormat: MongoFormat[Something] = io.sphere.mongo.generic.deriveMongoFormat +// val mongoFormat: TypedMongoFormat[Something] = io.sphere.mongo.generic.deriveMongoFormat // val something = mongoFormat.fromMongoValue(dbo) // something must be(Something(Some(3), 2)) // } -// } + } "mongoEnum" must { - "serialize and deserialize enums" in { - val mongo: MongoFormat[Color.Value] = io.sphere.mongo.generic.deriveMongoFormat - - // mongo java driver knows how to encode/decode Strings - val serializedObject = mongo.toMongoValue(Color.Red).asInstanceOf[String] - serializedObject must be("Red") - - val enumValue = mongo.fromMongoValue(serializedObject) - enumValue must be(Color.Red) - } +// "serialize and deserialize enums" in { +// val mongo: TypedMongoFormat[Color.Value] = io.sphere.mongo.generic.deriveMongoFormat +// +// // mongo java driver knows how to encode/decode Strings +// val serializedObject = mongo.toMongoValue(Color.Red).asInstanceOf[String] +// serializedObject must be("Red") +// +// val enumValue = mongo.fromMongoValue(serializedObject) +// enumValue must be(Color.Red) +// } } diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala new file mode 100644 index 00000000..0df5548a --- /dev/null +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala @@ -0,0 +1,96 @@ +package io.sphere.mongo.format + +import io.sphere.mongo.MongoUtils.* +import io.sphere.mongo.generic.* +import org.scalatest.OptionValues +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +object OptionMongoFormatSpec { + + case class SimpleClass(value1: String, value2: Int) + +// object SimpleClass { +// val mongo: TypedMongoFormat[SimpleClass] = deriveMongoFormat[SimpleClass] +// } + + case class ComplexClass(name: String, simpleClass: Option[SimpleClass]) + +// object ComplexClass { +// val mongo: TypedMongoFormat[ComplexClass] = deriveMongoFormat[ComplexClass] +// } + +} + +class OptionMongoFormatSpec extends AnyWordSpec with Matchers with OptionValues { + import OptionMongoFormatSpec.* + + "MongoFormat[Option[_]]" should { + "handle presence of all fields" in { + val dbo = dbObj( + "value1" -> "a", + "value2" -> 45 + ) + val result = deriveMongoFormat[Option[SimpleClass]].fromMongoValue(dbo) + result.value.value1 mustEqual "a" + result.value.value2 mustEqual 45 + } + + "handle presence of all fields mixed with ignored fields" in { + val dbo = dbObj( + "value1" -> "a", + "value2" -> 45, + "value3" -> "b" + ) + val result = deriveMongoFormat[Option[SimpleClass]].fromMongoValue(dbo) + result.value.value1 mustEqual "a" + result.value.value2 mustEqual 45 + } + + "handle presence of not all the fields" in pendingUntilFixed { + // TODO we need to implement default value handling to fix this + val dbo = dbObj("value1" -> "a") + an[Exception] mustBe thrownBy(deriveMongoFormat[Option[SimpleClass]].fromMongoValue(dbo)) + } + + "handle absence of all fields" in { + val dbo = dbObj() + val result = deriveMongoFormat[Option[SimpleClass]].fromMongoValue(dbo) + result mustEqual None + } + + "handle absence of all fields mixed with ignored fields" in { + val dbo = dbObj("value3" -> "a") + val result = deriveMongoFormat[Option[SimpleClass]].fromMongoValue(dbo) + result mustEqual None + } + +// "consider all fields if the data type does not impose any restriction" in { +// val dbo = dbObj( +// "key1" -> "value1", +// "key2" -> "value2" +// ) +// val expected = Map("key1" -> "value1", "key2" -> "value2") +// val result = deriveMongoFormat[Map[String, String]].fromMongoValue(dbo) +// result mustEqual expected +// +// val maybeResult = deriveMongoFormat[Option[Map[String, String]]].fromMongoValue(dbo) +// maybeResult.value mustEqual expected +// } + + "parse optional element" in { + val dbo = dbObj( + "name" -> "ze name", + "simpleClass" -> dbObj( + "value1" -> "value1", + "value2" -> 42 + ) + ) + val result = deriveMongoFormat[ComplexClass].fromMongoValue(dbo) + result.simpleClass.value.value1 mustEqual "value1" + result.simpleClass.value.value2 mustEqual 42 + + deriveMongoFormat[ComplexClass].toMongoValue(result) mustEqual dbo + } + } +} diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala new file mode 100644 index 00000000..d74fa9cc --- /dev/null +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala @@ -0,0 +1,125 @@ +package io.sphere.mongo.generic + +import io.sphere.mongo.MongoUtils.* +import io.sphere.mongo.format.DefaultMongoFormats.* +import io.sphere.mongo.format.MongoFormat +import org.scalatest.OptionValues +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import scala.util.Try + +object MongoEmbeddedSpec { + case class Embedded(value1: String, @MongoKey("_value2") value2: Int) + + case class Test1(name: String, @MongoEmbedded embedded: Embedded) + + case class Test2(name: String, @MongoEmbedded embedded: Option[Embedded] = None) + + case class Test3( + @MongoIgnore name: String = "default", + @MongoEmbedded embedded: Option[Embedded] = None) + + case class SubTest4(@MongoEmbedded embedded: Embedded) + + case class Test4(subField: Option[SubTest4] = None) +} + +class MongoEmbeddedSpec extends AnyWordSpec with Matchers with OptionValues { + import MongoEmbeddedSpec.* + + "MongoEmbedded" should { + "flatten the db object in one object" in { + val dbo = dbObj( + "name" -> "ze name", + "value1" -> "ze value1", + "_value2" -> 45 + ) + val test1 = deriveMongoFormat[Test1].fromMongoValue(dbo) + test1.name mustEqual "ze name" + test1.embedded.value1 mustEqual "ze value1" + test1.embedded.value2 mustEqual 45 + + val result = deriveMongoFormat[Test1].toMongoValue(test1) + result mustEqual dbo + } + + "validate that the db object contains all needed fields" in pendingUntilFixed { + // TODO default field + val dbo = dbObj( + "name" -> "ze name", + "value1" -> "ze value1" + ) + Try(deriveMongoFormat[Test1].fromMongoValue(dbo)).isFailure must be(true) + } + + "support optional embedded attribute" in { + val dbo = dbObj( + "name" -> "ze name", + "value1" -> "ze value1", + "_value2" -> 45 + ) + val test2 = deriveMongoFormat[Test2].fromMongoValue(dbo) + test2.name mustEqual "ze name" + test2.embedded.value.value1 mustEqual "ze value1" + test2.embedded.value.value2 mustEqual 45 + + val result = deriveMongoFormat[Test2].toMongoValue(test2) + result mustEqual dbo + } + + "ignore unknown fields" in { + val dbo = dbObj( + "name" -> "ze name", + "value1" -> "ze value1", + "_value2" -> 45, + "value4" -> true + ) + val test2 = deriveMongoFormat[Test2].fromMongoValue(dbo) + test2.name mustEqual "ze name" + test2.embedded.value.value1 mustEqual "ze value1" + test2.embedded.value.value2 mustEqual 45 + } + + "ignore ignored fields" in pendingUntilFixed { + // TODO Ignore + val dbo = dbObj( + "value1" -> "ze value1", + "_value2" -> 45 + ) + val test3 = deriveMongoFormat[Test3].fromMongoValue(dbo) + test3.name mustEqual "default" + test3.embedded.value.value1 mustEqual "ze value1" + test3.embedded.value.value2 mustEqual 45 + } + + "check for sub-fields" in { + val dbo = dbObj( + "subField" -> dbObj( + "value1" -> "ze value1", + "_value2" -> 45 + ) + ) + val test4 = deriveMongoFormat[Test4].fromMongoValue(dbo) + test4.subField.value.embedded.value1 mustEqual "ze value1" + test4.subField.value.embedded.value2 mustEqual 45 + } + + "support the absence of optional embedded attribute" in { + val dbo = dbObj( + "name" -> "ze name" + ) + val test2 = deriveMongoFormat[Test2].fromMongoValue(dbo) + test2.name mustEqual "ze name" + test2.embedded mustEqual None + } + + "validate the absence of some embedded attributes" in pendingUntilFixed { + val dbo = dbObj( + "name" -> "ze name", + "value1" -> "ze value1" + ) + Try(deriveMongoFormat[Test2].fromMongoValue(dbo)).isFailure must be(true) + } + } +} diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala new file mode 100644 index 00000000..f5a93c0e --- /dev/null +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala @@ -0,0 +1,43 @@ +package io.sphere.mongo.generic + +import com.mongodb.BasicDBObject +import io.sphere.mongo.format.DefaultMongoFormats.* +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import scala.jdk.CollectionConverters.* + +class MongoKeySpec extends AnyWordSpec with Matchers { + import MongoKeySpec.* + + "deriving MongoFormat" must { + "rename fields annotated with @MongoKey" in { + val test = + Test(value1 = "value1", value2 = "value2", subTest = SubTest(value2 = "other_value2")) + + val formatter = deriveMongoFormat[Test] + val dbo = formatter.toMongoValue(test) + val map = dbo.asInstanceOf[BasicDBObject].toMap.asScala.toMap[Any, Any] + + map.get("value1") must equal(Some("value1")) + map.get("value2") must equal(None) + map.get("new_value_2") must equal(Some("value2")) + map.get("new_sub_value_2") must equal(Some("other_value2")) + + val newTest = formatter.fromMongoValue(dbo) + newTest must be(test) + } + } +} + +object MongoKeySpec { + case class SubTest( + @MongoKey("new_sub_value_2") value2: String + ) + + case class Test( + value1: String, + @MongoKey("new_value_2") value2: String, + @MongoEmbedded subTest: SubTest + ) +} From dbee359a60cf0a755802a87240656bedbc25af49 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 17 May 2024 17:23:48 +0200 Subject: [PATCH 015/142] Add MongoTypeHint and MongoTypeHintField --- .../mongo/generic/AnnotationReader.scala | 89 +++--- .../io/sphere/mongo/generic/Annotations.scala | 6 +- .../io/sphere/mongo/generic/Derivation.scala | 30 +- ...goTypeHintFieldWithAbstractClassSpec.scala | 53 ++++ ...ongoTypeHintFieldWithSealedTraitSpec.scala | 54 ++++ .../mongo/generic/SumTypesDerivingSpec.scala | 269 ++++++++++++++++++ 6 files changed, 452 insertions(+), 49 deletions(-) create mode 100644 mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala create mode 100644 mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala create mode 100644 mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala index cac2c2d6..d095d638 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala @@ -5,35 +5,45 @@ import scala.quoted.{Expr, Quotes, Type, Varargs} private type MA = MongoAnnotation case class Field(name: String, embedded: Boolean, ignored: Boolean, mongoKey: Option[MongoKey]) { - val fieldName: String = mongoKey.map(_.newFieldName).getOrElse(name) + val fieldName: String = mongoKey.map(_.value).getOrElse(name) } -case class Annotations( +case class CaseClassMetaData( name: String, - forType: Vector[MA], - byField: Map[String, Vector[MA]], + typeHintRaw: Option[MongoTypeHint], fields: Vector[Field] -) +) { + val typeHint: Option[String] = + typeHintRaw.map(_.value).filterNot(_.toList.forall(_ == ' ')) +} -case class AllAnnotations( - top: Annotations, - subtypes: Map[String, Annotations] -) +case class TraitMetaData( + top: CaseClassMetaData, + typeHintFieldRaw: Option[MongoTypeHintField], + subtypes: Map[String, CaseClassMetaData] +) { + val typeDiscriminator: String = typeHintFieldRaw.map(_.value).getOrElse("type") +} -// TODO this can probably be simplifed later class AnnotationReader(using q: Quotes): import q.reflect.* - def readCaseClassMetaData[T: Type]: Expr[Annotations] = { + def readCaseClassMetaData[T: Type]: Expr[CaseClassMetaData] = { val sym = TypeRepr.of[T].typeSymbol - topAnnotations(sym) + caseClassMetaData(sym) } - def allAnnotations[T: Type]: Expr[AllAnnotations] = { + def readTraitMetaData[T: Type]: Expr[TraitMetaData] = { val sym = TypeRepr.of[T].typeSymbol + val typeHintField = + sym.annotations.map(findMongoTypeHintField).find(_.isDefined).flatten match { + case Some(thf) => '{ Some($thf) } + case None => '{ None } + } '{ - AllAnnotations( - top = ${ topAnnotations(sym) }, + TraitMetaData( + top = ${ caseClassMetaData(sym) }, + typeHintFieldRaw = $typeHintField, subtypes = ${ subtypeAnnotations(sym) } ) } @@ -51,48 +61,53 @@ class AnnotationReader(using q: Quotes): private def findKey(tree: Tree): Option[Expr[MongoKey]] = Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[MongoKey]).map(_.asExprOf[MongoKey]) + private def findTypeHint(tree: Tree): Option[Expr[MongoTypeHint]] = + Option + .when(tree.isExpr)(tree.asExpr) + .filter(_.isExprOf[MongoTypeHint]) + .map(_.asExprOf[MongoTypeHint]) + + private def findMongoTypeHintField(tree: Tree): Option[Expr[MongoTypeHintField]] = + Option + .when(tree.isExpr)(tree.asExpr) + .filter(_.isExprOf[MongoTypeHintField]) + .map(_.asExprOf[MongoTypeHintField]) + private def collectFieldInfo(s: Symbol): Expr[Field] = val embedded = Expr(s.annotations.exists(findEmbedded)) val ignored = Expr(s.annotations.exists(findIgnored)) val name = Expr(s.name) - s.annotations.map(findKey).find(_.isDefined).flatten match { - case Some(k) => - '{ Field(name = $name, embedded = $embedded, ignored = $ignored, mongoKey = Some($k)) } - case None => - '{ Field(name = $name, embedded = $embedded, ignored = $ignored, mongoKey = None) } + val mongoKey = s.annotations.map(findKey).find(_.isDefined).flatten match { + case Some(k) => '{ Some($k) } + case None => '{ None } } + '{ Field(name = $name, embedded = $embedded, ignored = $ignored, mongoKey = $mongoKey) } - private def fieldAnnotations(s: Symbol): Expr[(String, Vector[MA])] = - val annots = Varargs(s.annotations.flatMap(annotationTree)) - val name = Expr(s.name) - - '{ $name -> Vector($annots*) } - end fieldAnnotations - - private def topAnnotations(sym: Symbol): Expr[Annotations] = - val topAnns = Varargs(sym.annotations.flatMap(annotationTree)) + private def caseClassMetaData(sym: Symbol): Expr[CaseClassMetaData] = val caseParams = sym.primaryConstructor.paramSymss.take(1).flatten - val fieldAnns = Varargs(caseParams.map(fieldAnnotations)) val fields = Varargs(caseParams.map(collectFieldInfo)) val name = Expr(sym.name) + val typeHint = sym.annotations.map(findTypeHint).find(_.isDefined).flatten match { + case Some(th) => '{ Some($th) } + case None => '{ None } + } '{ - Annotations( + CaseClassMetaData( name = $name, - forType = Vector($topAnns*), - byField = Map($fieldAnns*), + typeHintRaw = $typeHint, fields = Vector($fields*) ) } - end topAnnotations + end caseClassMetaData - private def subtypeAnnotation(sym: Symbol): Expr[(String, Annotations)] = + private def subtypeAnnotation(sym: Symbol): Expr[(String, CaseClassMetaData)] = val name = Expr(sym.name) - val annots = topAnnotations(sym) + val annots = caseClassMetaData(sym) '{ ($name, $annots) } end subtypeAnnotation - private def subtypeAnnotations(sym: Symbol): Expr[Map[String, Annotations]] = + private def subtypeAnnotations(sym: Symbol): Expr[Map[String, CaseClassMetaData]] = val subtypes = Varargs(sym.children.map(subtypeAnnotation)) '{ Map($subtypes*) } diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Annotations.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Annotations.scala index 5ef08963..17d3bd6f 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Annotations.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Annotations.scala @@ -6,6 +6,6 @@ sealed trait MongoAnnotation extends StaticAnnotation case class MongoEmbedded() extends MongoAnnotation case class MongoIgnore() extends MongoAnnotation -case class MongoKey(newFieldName: String) extends MongoAnnotation -case class MongoTypeHintField(typeDiscriminator: String) extends MongoAnnotation -case class MongoTypeHint(newClassName: String) extends MongoAnnotation +case class MongoKey(value: String) extends MongoAnnotation +case class MongoTypeHintField(value: String) extends MongoAnnotation +case class MongoTypeHint(value: String) extends MongoAnnotation diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala index 77174a95..8c02e9fa 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala @@ -31,11 +31,16 @@ inline def deriveMongoFormat[A: TypedMongoFormat]: TypedMongoFormat[A] = summon object TypedMongoFormat: private val emptyFieldsSet: Vector[String] = Vector.empty - inline def readCaseClassMetaData[T]: Annotations = ${ readCaseClassMetaDataImpl[T] } + inline def readCaseClassMetaData[T]: CaseClassMetaData = ${ readCaseClassMetaDataImpl[T] } - private def readCaseClassMetaDataImpl[T: Type](using Quotes): Expr[Annotations] = + private def readCaseClassMetaDataImpl[T: Type](using Quotes): Expr[CaseClassMetaData] = AnnotationReader().readCaseClassMetaData[T] + inline def readTraitMetaData[T]: TraitMetaData = ${ readTraitMetaDataImpl[T] } + + private def readTraitMetaDataImpl[T: Type](using Quotes): Expr[TraitMetaData] = + AnnotationReader().readTraitMetaData[T] + inline given derive[A](using Mirror.Of[A]): TypedMongoFormat[A] = Derivation.derived given TypedMongoFormat[Int] = new NativeMongoFormat[Int] @@ -79,8 +84,12 @@ object TypedMongoFormat: inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): TypedMongoFormat[A] = new TypedMongoFormat[A]: - val annotations = readCaseClassMetaData[A] - val typeField = "typeDiscriminator" + val traitMetaData = readTraitMetaData[A] + val typeHintMap = traitMetaData.subtypes.collect { + case (name, classMeta) if classMeta.typeHint.isDefined => + name -> classMeta.typeHint.get + } + val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) val formatters = summonFormatters[mirrorOfSum.MirroredElemTypes] val names = constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector .asInstanceOf[Vector[String]] @@ -88,16 +97,19 @@ object TypedMongoFormat: override def toMongoValue(a: A): MongoType = // we never get a trait here, only classes, it's safe to assume Product - val typeName = a.asInstanceOf[Product].productPrefix - val bson = formattersByTypeName(typeName).toMongoValue(a).asInstanceOf[BasicDBObject] - bson.put(typeField, typeName) + val originalTypeName = a.asInstanceOf[Product].productPrefix + val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) + val bson = + formattersByTypeName(originalTypeName).toMongoValue(a).asInstanceOf[BasicDBObject] + bson.put(traitMetaData.typeDiscriminator, typeName) bson override def fromMongoValue(bson: MongoType): A = bson match case bson: BasicDBObject => - val typeName = bson.get(typeField).asInstanceOf[String] - formattersByTypeName(typeName).fromMongoValue(bson).asInstanceOf[A] + val typeName = bson.get(traitMetaData.typeDiscriminator).asInstanceOf[String] + val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) + formattersByTypeName(originalTypeName).fromMongoValue(bson).asInstanceOf[A] case x => throw new Exception(s"BsonObject is expected for a Trait subtype, instead got $x") end deriveTrait diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala new file mode 100644 index 00000000..2848f411 --- /dev/null +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala @@ -0,0 +1,53 @@ +package io.sphere.mongo.generic + +import io.sphere.mongo.MongoUtils.dbObj +import io.sphere.mongo.format.DefaultMongoFormats.* +import io.sphere.mongo.format.{MongoFormat, fromMongo, toMongo} +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class MongoTypeHintFieldWithAbstractClassSpec extends AnyWordSpec with Matchers { + import MongoTypeHintFieldWithAbstractClassSpec.* + + "MongoTypeHintField (with abstract class)" must { + "allow to set another field to distinguish between types (toMongo)" in { + val user = UserWithPicture("foo-123", Medium, "http://example.com") + val expected = dbObj( + "userId" -> "foo-123", + "pictureSize" -> dbObj("pictureType" -> "Medium"), + "pictureUrl" -> "http://example.com") + + val dbo = UserWithPicture.mongo.toMongoValue(user) + dbo must be(expected) + } + + "allow to set another field to distinguish between types (fromMongo)" in { + val initialDbo = dbObj( + "userId" -> "foo-123", + "pictureSize" -> dbObj("pictureType" -> "Medium"), + "pictureUrl" -> "http://example.com") + + val user = UserWithPicture.mongo.fromMongoValue(initialDbo) + + user must be(UserWithPicture("foo-123", Medium, "http://example.com")) + + val dbo = UserWithPicture.mongo.toMongoValue(user) + dbo must be(initialDbo) + } + } +} + +object MongoTypeHintFieldWithAbstractClassSpec { + + @MongoTypeHintField(value = "pictureType") + sealed abstract class PictureSize + case object Small extends PictureSize + case object Medium extends PictureSize + case object Big extends PictureSize + + case class UserWithPicture(userId: String, pictureSize: PictureSize, pictureUrl: String) + + object UserWithPicture { + val mongo: TypedMongoFormat[UserWithPicture] = deriveMongoFormat[UserWithPicture] + } +} diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala new file mode 100644 index 00000000..b356d692 --- /dev/null +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala @@ -0,0 +1,54 @@ +package io.sphere.mongo.generic + +import io.sphere.mongo.MongoUtils.dbObj +import io.sphere.mongo.format.DefaultMongoFormats.* +import io.sphere.mongo.format.{MongoFormat, fromMongo, toMongo} +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class MongoTypeHintFieldWithSealedTraitSpec extends AnyWordSpec with Matchers { + import MongoTypeHintFieldWithSealedTraitSpec.* + + "MongoTypeHintField (with sealed trait)" must { + "allow to set another field to distinguish between types (toMongo)" in { + val user = UserWithPicture("foo-123", Medium, "http://example.com") + val expected = dbObj( + "userId" -> "foo-123", + "pictureSize" -> dbObj("pictureType" -> "Medium"), + "pictureUrl" -> "http://example.com") + + val dbo = userWithPictureFormat.toMongoValue(user) + dbo must be(expected) + } + + "allow to set another field to distinguish between types (fromMongo)" in { + val initialDbo = dbObj( + "userId" -> "foo-123", + "pictureSize" -> dbObj("pictureType" -> "Medium"), + "pictureUrl" -> "http://example.com") + + val user = userWithPictureFormat.fromMongoValue(initialDbo) + + user must be(UserWithPicture("foo-123", Medium, "http://example.com")) + + val dbo = userWithPictureFormat.toMongoValue(user) + dbo must be(initialDbo) + } + } +} + +object MongoTypeHintFieldWithSealedTraitSpec { + + // issue https://github.com/commercetools/sphere-scala-libs/issues/10 + // @MongoTypeHintField must be repeated for all sub-classes + @MongoTypeHintField(value = "pictureType") + sealed trait PictureSize + case object Small extends PictureSize + case object Medium extends PictureSize + case object Big extends PictureSize + + case class UserWithPicture(userId: String, pictureSize: PictureSize, pictureUrl: String) + + val userWithPictureFormat = deriveMongoFormat[UserWithPicture] + +} diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala new file mode 100644 index 00000000..cb7c8d40 --- /dev/null +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala @@ -0,0 +1,269 @@ +package io.sphere.mongo.generic + +import com.mongodb.DBObject +import io.sphere.mongo.MongoUtils.dbObj +import io.sphere.mongo.format.DefaultMongoFormats.* +import io.sphere.mongo.format.MongoFormat +import org.bson.BSONObject +import org.scalatest.Assertion +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class SumTypesDerivingSpec extends AnyWordSpec with Matchers { + import SumTypesDerivingSpec.* + + "Serializing sum types" must { + "use 'type' as default field" in { + check(Color1.format, Color1.Red, dbObj("type" -> "Red")) + + check(Color1.format, Color1.Custom("2356"), dbObj("type" -> "Custom", "rgb" -> "2356")) + } + + "use custom field" in { + check(Color2.format, Color2.Red, dbObj("color" -> "Red")) + + check(Color2.format, Color2.Custom("2356"), dbObj("color" -> "Custom", "rgb" -> "2356")) + } + + "use custom values" in { + check(Color3.format, Color3.Red, dbObj("type" -> "red")) + + check(Color3.format, Color3.Custom("2356"), dbObj("type" -> "custom", "rgb" -> "2356")) + } + + "use custom field & values" in { + check(Color4.format, Color4.Red, dbObj("color" -> "red")) + + check(Color4.format, Color4.Custom("2356"), dbObj("color" -> "custom", "rgb" -> "2356")) + } + + "not allow specifying different custom field" in pendingUntilFixed { + // to serialize Custom, should we use type "color" or "color-custom"? + "deriveMongoFormat[Color5]" mustNot compile + } + + "not allow specifying different custom field on intermediate level" in { + // to serialize Custom, should we use type "color" or "color-custom"? + "deriveMongoFormat[Color6]" mustNot compile + } + + "use intermediate level" in { + Color7.format + } + + "do not use sealed trait info when using a case class directly" in { + check(Color8.format, Color8.Custom("2356"), dbObj("type" -> "Custom", "rgb" -> "2356")) + + check(Color8.Custom.format, Color8.Custom("2356"), dbObj("rgb" -> "2356")) + + // unless annotated + + check( + Color8.format, + Color8.CustomAnnotated("2356"), + dbObj("type" -> "CustomAnnotated", "rgb" -> "2356")) +// TODO should this be a thing? +// check( +// Color8.CustomAnnotated.format, +// Color8.CustomAnnotated("2356"), +// dbObj("type" -> "CustomAnnotated", "rgb" -> "2356")) + } + + "use default values if custom values are empty" in { + check(Color9.format, Color9.Red, dbObj("type" -> "Red")) + + check(Color9.format, Color9.Custom("2356"), dbObj("type" -> "Custom", "rgb" -> "2356")) + } + + "allow for providing custom instances for objects" in { + check(Color10.format, Color10.Red, dbObj("type" -> "Red", "extraField" -> "panda")) + + check(Color10.format, Color10.Custom("2356"), dbObj("type" -> "Custom", "rgb" -> "2356")) + } + + "allow for providing custom instances for classes" in { + check(Color11.format, Color11.Red, dbObj("type" -> "Red")) + + check( + Color11.format, + Color11.Custom("2356"), + dbObj("type" -> "Custom", "rgb" -> "2356", "extraField" -> "panda")) + } + + "allow for providing custom instances for classes when the class has a type parameter with an upper bound" in { + check(ColorUpperBound.format, ColorUpperBound.Red, dbObj("type" -> "Red")) + + check( + ColorUpperBound.format, + ColorUpperBound.Custom("2356"), + dbObj("type" -> "Custom", "rgb" -> "2356", "extraField" -> "panda")) + } + + "allow for providing custom instances for classes when the class has an unbounded type parameter" in { + check(ColorUnbound.format, ColorUnbound.Red, dbObj("type" -> "Red")) + + check( + ColorUnbound.format, + ColorUnbound.Custom("2356"), + dbObj("type" -> "Custom", "rgb" -> "2356", "extraField" -> "panda")) + } + + } +} + +object SumTypesDerivingSpec { + import Matchers.* + + def check[A, B <: A](format: TypedMongoFormat[A], b: B, dbo: DBObject): Assertion = { + val serialized = format.toMongoValue(b) + serialized must be(dbo) + + format.fromMongoValue(serialized) must be(b) + } + + sealed trait Color1 + object Color1 { + case object Red extends Color1 + case class Custom(rgb: String) extends Color1 + val format = deriveMongoFormat[Color1] + } + + @MongoTypeHintField("color") + sealed trait Color2 + object Color2 { + case object Red extends Color2 + case class Custom(rgb: String) extends Color2 + val format = deriveMongoFormat[Color2] + } + + sealed trait Color3 + object Color3 { + @MongoTypeHint("red") case object Red extends Color3 + @MongoTypeHint("custom") case class Custom(rgb: String) extends Color3 + val format = deriveMongoFormat[Color3] + } + + @MongoTypeHintField("color") + sealed trait Color4 + object Color4 { + @MongoTypeHint("red") case object Red extends Color4 + @MongoTypeHint("custom") case class Custom(rgb: String) extends Color4 + val format = deriveMongoFormat[Color4] + } + + @MongoTypeHintField("color") + sealed trait Color5 + object Color5 { + @MongoTypeHint("red") + case object Red extends Color5 + @MongoTypeHintField("color-custom") + @MongoTypeHint("custom") + case class Custom(rgb: String) extends Color5 + } + + @MongoTypeHintField("color") + sealed trait Color6 + object Color6 { + @MongoTypeHintField("color-custom") + abstract class MyColor extends Color6 + @MongoTypeHint("red") + case object Red extends MyColor + @MongoTypeHint("custom") + case class Custom(rgb: String) extends MyColor + } + + sealed trait Color7 + sealed trait Color7a extends Color7 + object Color7 { + case object Red extends Color7a + case class Custom(rgb: String) extends Color7a + def format = deriveMongoFormat[Color7] + } + + sealed trait Color8 + object Color8 { + // the formats must use `lazy` to make this code compile + + case object Red extends Color8 + case class Custom(rgb: String) extends Color8 + object Custom { + lazy val format = deriveMongoFormat[Custom] + } + @MongoTypeHintField("type") + case class CustomAnnotated(rgb: String) extends Color8 + object CustomAnnotated { + lazy val format = deriveMongoFormat[CustomAnnotated] + } + lazy val format = deriveMongoFormat[Color8] + } + + sealed trait Color9 + object Color9 { + @MongoTypeHint("") + case object Red extends Color9 + @MongoTypeHint(" ") + case class Custom(rgb: String) extends Color9 + val format = deriveMongoFormat[Color9] + } + + sealed trait Color10 + object Color10 { + case object Red extends Color10 + case class Custom(rgb: String) extends Color10 + + implicit val redFormatter: TypedMongoFormat[Red.type] = new TypedMongoFormat[Red.type] { + override def toMongoValue(a: Red.type): MongoType = dbObj("type" -> "Red", "extraField" -> "panda") + override def fromMongoValue(any: MongoType): Red.type = Red + } + val format = deriveMongoFormat[Color10] + } + + sealed trait Color11 + object Color11 { + case object Red extends Color11 + case class Custom(rgb: String) extends Color11 + + implicit val customFormatter: TypedMongoFormat[Custom] = new TypedMongoFormat[Custom] { + override def toMongoValue(a: Custom): MongoType = + dbObj("type" -> "Custom", "rgb" -> a.rgb, "extraField" -> "panda") + override def fromMongoValue(any: MongoType): Custom = + Custom(any.asInstanceOf[BSONObject].get("rgb").asInstanceOf[String]) + } + val format = deriveMongoFormat[Color11] + } + + sealed trait ColorUpperBound + object ColorUpperBound { + + sealed trait Bound + case object B1 extends Bound + case class B2(int: Int) extends Bound + + case object Red extends ColorUpperBound + case class Custom[Type1 <: Bound](rgb: String) extends ColorUpperBound + + implicit def customFormatter[A <: Bound]: TypedMongoFormat[Custom[A]] = new TypedMongoFormat[Custom[A]] { + override def toMongoValue(a: Custom[A]): MongoType = + dbObj("type" -> "Custom", "rgb" -> a.rgb, "extraField" -> "panda") + override def fromMongoValue(any: MongoType): Custom[A] = + Custom(any.asInstanceOf[BSONObject].get("rgb").asInstanceOf[String]) + } + + val format = deriveMongoFormat[ColorUpperBound] + } + + sealed trait ColorUnbound + object ColorUnbound { + case object Red extends ColorUnbound + case class Custom[A](rgb: String) extends ColorUnbound + + implicit def customFormatter[A]: TypedMongoFormat[Custom[A]] = new TypedMongoFormat[Custom[A]] { + override def toMongoValue(a: Custom[A]): MongoType = + dbObj("type" -> "Custom", "rgb" -> a.rgb, "extraField" -> "panda") + override def fromMongoValue(any: MongoType): Custom[A] = + Custom(any.asInstanceOf[BSONObject].get("rgb").asInstanceOf[String]) + } + + val format = deriveMongoFormat[ColorUnbound] + } +} From 02388ea9619d4ea754df8015c5566a9b0b55fc2a Mon Sep 17 00:00:00 2001 From: Marcelo Gomes Date: Thu, 23 May 2024 12:11:47 +0200 Subject: [PATCH 016/142] ENE-49 Fix infinite loop in derivation --- .../io/sphere/mongo/generic/Derivation.scala | 6 ++-- .../io/sphere/mongo/SerializationTest.scala | 31 ++++++++++++++----- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala index 8c02e9fa..eee2ac09 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala @@ -27,9 +27,11 @@ private final class NativeMongoFormat[A <: SimpleMongoType] extends TypedMongoFo def fromMongoValue(any: MongoType): A = any.asInstanceOf[A] } -inline def deriveMongoFormat[A: TypedMongoFormat]: TypedMongoFormat[A] = summon +inline def deriveMongoFormat[A](using Mirror.Of[A]): TypedMongoFormat[A] = TypedMongoFormat.derived object TypedMongoFormat: + inline def apply[A: TypedMongoFormat]: TypedMongoFormat[A] = summon + private val emptyFieldsSet: Vector[String] = Vector.empty inline def readCaseClassMetaData[T]: CaseClassMetaData = ${ readCaseClassMetaDataImpl[T] } @@ -41,7 +43,7 @@ object TypedMongoFormat: private def readTraitMetaDataImpl[T: Type](using Quotes): Expr[TraitMetaData] = AnnotationReader().readTraitMetaData[T] - inline given derive[A](using Mirror.Of[A]): TypedMongoFormat[A] = Derivation.derived + inline given derived[A](using Mirror.Of[A]): TypedMongoFormat[A] = Derivation.derived given TypedMongoFormat[Int] = new NativeMongoFormat[Int] given TypedMongoFormat[String] = new NativeMongoFormat[String] diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala index 3aac6aa7..c88267c4 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala @@ -6,8 +6,12 @@ import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec object SerializationTest: + // For semi-automatic derivarion case class Something(a: Option[Int], b: Int = 2) + // For Automatic derivation with `derives` + case class Frunfles(a: Option[Int], b: Int = 2) derives TypedMongoFormat + object Color extends Enumeration: val Blue, Red, Yellow = Value @@ -24,23 +28,36 @@ class SerializationTest extends AnyWordSpec with Matchers: dbo.put("a", Integer.valueOf(3)) dbo.put("b", Integer.valueOf(4)) - // TODO what is this? :D - val med: TypedMongoFormat[Medium.type] = io.sphere.mongo.generic.deriveMongoFormat + // Using backwards-compatible `deriveMongoFormat` + given TypedMongoFormat[Something] = io.sphere.mongo.generic.deriveMongoFormat - val mongoFormat: TypedMongoFormat[Something] = io.sphere.mongo.generic.deriveMongoFormat - val something = mongoFormat.fromMongoValue(dbo) + val something = TypedMongoFormat[Something].fromMongoValue(dbo) something mustBe Something(Some(3), 4) } "generate a format that serializes optional fields with value None as BSON objects without that field" in { - val testFormat: TypedMongoFormat[Something] = io.sphere.mongo.generic.deriveMongoFormat + // Using new Scala 3 `derived` special method + given TypedMongoFormat[Something] = TypedMongoFormat.derived val something = Something(None, 1) - val serializedObject = testFormat.toMongoValue(something).asInstanceOf[BasicDBObject] + val serializedObject = + TypedMongoFormat[Something].toMongoValue(something).asInstanceOf[BasicDBObject] + serializedObject.keySet().contains("b") must be(true) serializedObject.keySet().contains("a") must be(false) + TypedMongoFormat[Something].fromMongoValue(serializedObject) must be(something) + } + + "generate a format that serializes optional fields with value None as BSON objects without that field (using derives)" in { + // Using an automatically-derived type via new Scala 3 `derives` directive + val frunfles = Frunfles(None, 1) - testFormat.fromMongoValue(serializedObject) must be(something) + val serializedObject = + TypedMongoFormat[Frunfles].toMongoValue(frunfles).asInstanceOf[BasicDBObject] + + serializedObject.keySet().contains("b") must be(true) + serializedObject.keySet().contains("a") must be(false) + TypedMongoFormat[Frunfles].fromMongoValue(serializedObject) must be(frunfles) } // "generate a format that use default values" in { From 9298ec3ffb6e928a8397c8ed442282af06ae42fc Mon Sep 17 00:00:00 2001 From: Marcelo Gomes Date: Thu, 23 May 2024 12:20:34 +0200 Subject: [PATCH 017/142] ENE-49 Add a case with `implicit` keyword --- .../src/test/scala/io/sphere/mongo/SerializationTest.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala index c88267c4..96d4024d 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala @@ -28,15 +28,15 @@ class SerializationTest extends AnyWordSpec with Matchers: dbo.put("a", Integer.valueOf(3)) dbo.put("b", Integer.valueOf(4)) - // Using backwards-compatible `deriveMongoFormat` - given TypedMongoFormat[Something] = io.sphere.mongo.generic.deriveMongoFormat + // Using backwards-compatible `deriveMongoFormat` + `implicit` + implicit val x: TypedMongoFormat[Something] = io.sphere.mongo.generic.deriveMongoFormat val something = TypedMongoFormat[Something].fromMongoValue(dbo) something mustBe Something(Some(3), 4) } "generate a format that serializes optional fields with value None as BSON objects without that field" in { - // Using new Scala 3 `derived` special method + // Using new Scala 3 `derived` special method + `given` given TypedMongoFormat[Something] = TypedMongoFormat.derived val something = Something(None, 1) From 8ced2154fa7276537c661c6b4343fc4b01ed37ba Mon Sep 17 00:00:00 2001 From: Peter Empen Date: Thu, 23 May 2024 12:34:15 +0200 Subject: [PATCH 018/142] add DefaultValuesSpec --- .../mongo/generic/DefaultValuesSpec.scala | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala new file mode 100644 index 00000000..6a5edad2 --- /dev/null +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala @@ -0,0 +1,31 @@ +package io.sphere.mongo.generic + +import io.sphere.mongo.MongoUtils.* +import io.sphere.mongo.format.DefaultMongoFormats.* +import io.sphere.mongo.format.MongoFormat +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class DefaultValuesSpec extends AnyWordSpec with Matchers: + import DefaultValuesSpec.* + + "deriving MongoFormat" must { + "handle default values" in { + val dbo = dbObj() + val test = MongoFormat[Test].fromMongoValue(dbo) + test.value1 mustBe "hello" + test.value2 mustBe None + test.value3 mustBe None + test.value4 mustBe Some("hi") + } + } + +object DefaultValuesSpec: + case class Test( + value1: String = "hello", + value2: Option[String], + value3: Option[String] = None, + value4: Option[String] = Some("hi") + ) + object Test: + implicit val mongo: MongoFormat[Test] = mongoProduct(apply _) From e4ccd28e54752f7b13cfe5db70fd7bf143f02d5b Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 23 May 2024 14:08:45 +0200 Subject: [PATCH 019/142] Fix OptionMongoFormatSpec --- .../mongo/format/OptionMongoFormatSpec.scala | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala index 0df5548a..eb6e7aa6 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala @@ -10,15 +10,15 @@ object OptionMongoFormatSpec { case class SimpleClass(value1: String, value2: Int) -// object SimpleClass { -// val mongo: TypedMongoFormat[SimpleClass] = deriveMongoFormat[SimpleClass] -// } + object SimpleClass { + given TypedMongoFormat[SimpleClass] = deriveMongoFormat[SimpleClass] + } case class ComplexClass(name: String, simpleClass: Option[SimpleClass]) -// object ComplexClass { -// val mongo: TypedMongoFormat[ComplexClass] = deriveMongoFormat[ComplexClass] -// } + object ComplexClass { + given TypedMongoFormat[ComplexClass] = deriveMongoFormat[ComplexClass] + } } @@ -31,7 +31,7 @@ class OptionMongoFormatSpec extends AnyWordSpec with Matchers with OptionValues "value1" -> "a", "value2" -> 45 ) - val result = deriveMongoFormat[Option[SimpleClass]].fromMongoValue(dbo) + val result = TypedMongoFormat[Option[SimpleClass]].fromMongoValue(dbo) result.value.value1 mustEqual "a" result.value.value2 mustEqual 45 } @@ -42,7 +42,7 @@ class OptionMongoFormatSpec extends AnyWordSpec with Matchers with OptionValues "value2" -> 45, "value3" -> "b" ) - val result = deriveMongoFormat[Option[SimpleClass]].fromMongoValue(dbo) + val result = TypedMongoFormat[Option[SimpleClass]].fromMongoValue(dbo) result.value.value1 mustEqual "a" result.value.value2 mustEqual 45 } @@ -50,18 +50,18 @@ class OptionMongoFormatSpec extends AnyWordSpec with Matchers with OptionValues "handle presence of not all the fields" in pendingUntilFixed { // TODO we need to implement default value handling to fix this val dbo = dbObj("value1" -> "a") - an[Exception] mustBe thrownBy(deriveMongoFormat[Option[SimpleClass]].fromMongoValue(dbo)) + an[Exception] mustBe thrownBy(TypedMongoFormat[Option[SimpleClass]].fromMongoValue(dbo)) } "handle absence of all fields" in { val dbo = dbObj() - val result = deriveMongoFormat[Option[SimpleClass]].fromMongoValue(dbo) + val result = TypedMongoFormat[Option[SimpleClass]].fromMongoValue(dbo) result mustEqual None } "handle absence of all fields mixed with ignored fields" in { val dbo = dbObj("value3" -> "a") - val result = deriveMongoFormat[Option[SimpleClass]].fromMongoValue(dbo) + val result = TypedMongoFormat[Option[SimpleClass]].fromMongoValue(dbo) result mustEqual None } @@ -86,11 +86,11 @@ class OptionMongoFormatSpec extends AnyWordSpec with Matchers with OptionValues "value2" -> 42 ) ) - val result = deriveMongoFormat[ComplexClass].fromMongoValue(dbo) + val result = TypedMongoFormat[ComplexClass].fromMongoValue(dbo) result.simpleClass.value.value1 mustEqual "value1" result.simpleClass.value.value2 mustEqual 42 - deriveMongoFormat[ComplexClass].toMongoValue(result) mustEqual dbo + TypedMongoFormat[ComplexClass].toMongoValue(result) mustEqual dbo } } } From 244dd515323f54594750a2ab5cbf26ecdcb6ad7e Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 23 May 2024 14:25:52 +0200 Subject: [PATCH 020/142] Refactor AnnotationReader macro functions and Remove bad imports. --- .../mongo/generic/AnnotationReader.scala | 12 ++++++++++++ .../io/sphere/mongo/generic/Derivation.scala | 19 ++++++------------- .../io/sphere/mongo/DerivationSpec.scala | 10 ++++++++-- .../io/sphere/mongo/SerializationTest.scala | 2 +- .../mongo/generic/MongoEmbeddedSpec.scala | 1 - .../sphere/mongo/generic/MongoKeySpec.scala | 1 - ...goTypeHintFieldWithAbstractClassSpec.scala | 1 - ...ongoTypeHintFieldWithSealedTraitSpec.scala | 1 - .../mongo/generic/SumTypesDerivingSpec.scala | 1 - 9 files changed, 27 insertions(+), 21 deletions(-) diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala index d095d638..c56f94e2 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala @@ -24,6 +24,18 @@ case class TraitMetaData( val typeDiscriminator: String = typeHintFieldRaw.map(_.value).getOrElse("type") } +object AnnotationReader { + inline def readTraitMetaData[T]: TraitMetaData = ${ readTraitMetaDataImpl[T] } + + inline def readCaseClassMetaData[T]: CaseClassMetaData = ${ readCaseClassMetaDataImpl[T] } + + private def readCaseClassMetaDataImpl[T: Type](using Quotes): Expr[CaseClassMetaData] = + AnnotationReader().readCaseClassMetaData[T] + + private def readTraitMetaDataImpl[T: Type](using Quotes): Expr[TraitMetaData] = + AnnotationReader().readTraitMetaData[T] +} + class AnnotationReader(using q: Quotes): import q.reflect.* diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala index eee2ac09..b579f44f 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala @@ -33,20 +33,11 @@ object TypedMongoFormat: inline def apply[A: TypedMongoFormat]: TypedMongoFormat[A] = summon private val emptyFieldsSet: Vector[String] = Vector.empty - inline def readCaseClassMetaData[T]: CaseClassMetaData = ${ readCaseClassMetaDataImpl[T] } - - private def readCaseClassMetaDataImpl[T: Type](using Quotes): Expr[CaseClassMetaData] = - AnnotationReader().readCaseClassMetaData[T] - - inline def readTraitMetaData[T]: TraitMetaData = ${ readTraitMetaDataImpl[T] } - - private def readTraitMetaDataImpl[T: Type](using Quotes): Expr[TraitMetaData] = - AnnotationReader().readTraitMetaData[T] - - inline given derived[A](using Mirror.Of[A]): TypedMongoFormat[A] = Derivation.derived given TypedMongoFormat[Int] = new NativeMongoFormat[Int] + given TypedMongoFormat[String] = new NativeMongoFormat[String] + given TypedMongoFormat[Boolean] = new NativeMongoFormat[Boolean] given [A](using TypedMongoFormat[A]): TypedMongoFormat[Option[A]] = @@ -68,6 +59,8 @@ object TypedMongoFormat: else Some(summon[TypedMongoFormat[A]].fromMongoValue(bson)) case MongoNothing => None // This can't happen, but it makes the compiler happy + inline given derived[A](using Mirror.Of[A]): TypedMongoFormat[A] = Derivation.derived + private def addField(bson: BasicDBObject, field: Field, mongoType: MongoType) = mongoType match case s: SimpleMongoType => bson.put(field.fieldName, s) @@ -86,7 +79,7 @@ object TypedMongoFormat: inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): TypedMongoFormat[A] = new TypedMongoFormat[A]: - val traitMetaData = readTraitMetaData[A] + val traitMetaData = AnnotationReader.readTraitMetaData[A] val typeHintMap = traitMetaData.subtypes.collect { case (name, classMeta) if classMeta.typeHint.isDefined => name -> classMeta.typeHint.get @@ -119,7 +112,7 @@ object TypedMongoFormat: inline private def deriveCaseClass[A]( mirrorOfProduct: Mirror.ProductOf[A]): TypedMongoFormat[A] = new TypedMongoFormat[A]: - val caseClassMetaData = readCaseClassMetaData[A] + val caseClassMetaData = AnnotationReader.readCaseClassMetaData[A] val formatters = summonFormatters[mirrorOfProduct.MirroredElemTypes] val fieldsAndFormatters = caseClassMetaData.fields.zip(formatters) diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala index 33c6e50a..b8e671d1 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala @@ -1,6 +1,12 @@ package io.sphere.mongo.format -import io.sphere.mongo.generic.{MongoEmbedded, MongoKey, MongoTypeHintField, TypedMongoFormat} +import io.sphere.mongo.generic.{ + AnnotationReader, + MongoEmbedded, + MongoKey, + MongoTypeHintField, + TypedMongoFormat +} import io.sphere.mongo.generic.TypedMongoFormat.* import org.scalatest.wordspec.AnyWordSpec import org.scalatest.matchers.must.Matchers @@ -49,7 +55,7 @@ class DerivationSpec extends AnyWordSpec with Matchers: case object Object2 extends Root case class Class(i: Int, @MongoEmbedded inner: InnerClass) extends Root - val res = readCaseClassMetaData[Root] + val res = AnnotationReader.readCaseClassMetaData[Root] } } diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala index 96d4024d..30f86ba5 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala @@ -1,7 +1,7 @@ package io.sphere.mongo import com.mongodb.BasicDBObject -import io.sphere.mongo.generic.TypedMongoFormat +import io.sphere.mongo.generic.{DefaultMongoFormats, TypedMongoFormat} import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala index d74fa9cc..6cbded76 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala @@ -1,7 +1,6 @@ package io.sphere.mongo.generic import io.sphere.mongo.MongoUtils.* -import io.sphere.mongo.format.DefaultMongoFormats.* import io.sphere.mongo.format.MongoFormat import org.scalatest.OptionValues import org.scalatest.matchers.must.Matchers diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala index f5a93c0e..a4b03f6b 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala @@ -1,7 +1,6 @@ package io.sphere.mongo.generic import com.mongodb.BasicDBObject -import io.sphere.mongo.format.DefaultMongoFormats.* import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala index 2848f411..3aa33269 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala @@ -1,7 +1,6 @@ package io.sphere.mongo.generic import io.sphere.mongo.MongoUtils.dbObj -import io.sphere.mongo.format.DefaultMongoFormats.* import io.sphere.mongo.format.{MongoFormat, fromMongo, toMongo} import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala index b356d692..ddd9996a 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala @@ -1,7 +1,6 @@ package io.sphere.mongo.generic import io.sphere.mongo.MongoUtils.dbObj -import io.sphere.mongo.format.DefaultMongoFormats.* import io.sphere.mongo.format.{MongoFormat, fromMongo, toMongo} import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala index cb7c8d40..48fff449 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala @@ -2,7 +2,6 @@ package io.sphere.mongo.generic import com.mongodb.DBObject import io.sphere.mongo.MongoUtils.dbObj -import io.sphere.mongo.format.DefaultMongoFormats.* import io.sphere.mongo.format.MongoFormat import org.bson.BSONObject import org.scalatest.Assertion From cccf06723ab27ab6fa2bc99eb8f94ac21a543a5f Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 23 May 2024 14:54:32 +0200 Subject: [PATCH 021/142] Move TypedMongoFormat instances to DefaultMongoFormats --- .../io/sphere/mongo/generic/Derivation.scala | 25 ------------------- .../io/sphere/mongo/DerivationSpec.scala | 3 ++- .../io/sphere/mongo/SerializationTest.scala | 1 + .../mongo/format/OptionMongoFormatSpec.scala | 1 + .../mongo/generic/MongoEmbeddedSpec.scala | 2 +- .../sphere/mongo/generic/MongoKeySpec.scala | 1 + ...goTypeHintFieldWithAbstractClassSpec.scala | 1 + ...ongoTypeHintFieldWithSealedTraitSpec.scala | 1 + .../mongo/generic/SumTypesDerivingSpec.scala | 19 ++++++++------ 9 files changed, 19 insertions(+), 35 deletions(-) diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala index b579f44f..7cf5aae3 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala @@ -34,31 +34,6 @@ object TypedMongoFormat: private val emptyFieldsSet: Vector[String] = Vector.empty - given TypedMongoFormat[Int] = new NativeMongoFormat[Int] - - given TypedMongoFormat[String] = new NativeMongoFormat[String] - - given TypedMongoFormat[Boolean] = new NativeMongoFormat[Boolean] - - given [A](using TypedMongoFormat[A]): TypedMongoFormat[Option[A]] = - new TypedMongoFormat[Option[A]]: - override def toMongoValue(a: Option[A]): MongoType = - a match - case Some(value) => summon[TypedMongoFormat[A]].toMongoValue(value) - case None => MongoNothing - - override def fromMongoValue(mongoType: MongoType): Option[A] = - val fieldNames = summon[TypedMongoFormat[A]].fieldNames - if (mongoType == null) None - else - mongoType match - case s: SimpleMongoType => Some(summon[TypedMongoFormat[A]].fromMongoValue(s)) - case bson: BasicDBObject => - val bsonFieldNames = bson.keySet().toArray - if (fieldNames.nonEmpty && bsonFieldNames.intersect(fieldNames).isEmpty) None - else Some(summon[TypedMongoFormat[A]].fromMongoValue(bson)) - case MongoNothing => None // This can't happen, but it makes the compiler happy - inline given derived[A](using Mirror.Of[A]): TypedMongoFormat[A] = Derivation.derived private def addField(bson: BasicDBObject, field: Field, mongoType: MongoType) = diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala index b8e671d1..7ffdf4b5 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala @@ -1,4 +1,4 @@ -package io.sphere.mongo.format +package io.sphere.mongo import io.sphere.mongo.generic.{ AnnotationReader, @@ -7,6 +7,7 @@ import io.sphere.mongo.generic.{ MongoTypeHintField, TypedMongoFormat } +import io.sphere.mongo.generic.DefaultMongoFormats.given import io.sphere.mongo.generic.TypedMongoFormat.* import org.scalatest.wordspec.AnyWordSpec import org.scalatest.matchers.must.Matchers diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala index 30f86ba5..738e1cfd 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala @@ -2,6 +2,7 @@ package io.sphere.mongo import com.mongodb.BasicDBObject import io.sphere.mongo.generic.{DefaultMongoFormats, TypedMongoFormat} +import io.sphere.mongo.generic.DefaultMongoFormats.given import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala index eb6e7aa6..ee435285 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala @@ -2,6 +2,7 @@ package io.sphere.mongo.format import io.sphere.mongo.MongoUtils.* import io.sphere.mongo.generic.* +import io.sphere.mongo.generic.DefaultMongoFormats.given import org.scalatest.OptionValues import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala index 6cbded76..e264acca 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala @@ -1,7 +1,7 @@ package io.sphere.mongo.generic import io.sphere.mongo.MongoUtils.* -import io.sphere.mongo.format.MongoFormat +import io.sphere.mongo.generic.DefaultMongoFormats.given import org.scalatest.OptionValues import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala index a4b03f6b..47e223db 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala @@ -1,6 +1,7 @@ package io.sphere.mongo.generic import com.mongodb.BasicDBObject +import io.sphere.mongo.generic.DefaultMongoFormats.given import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala index 3aa33269..c9de53f1 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala @@ -2,6 +2,7 @@ package io.sphere.mongo.generic import io.sphere.mongo.MongoUtils.dbObj import io.sphere.mongo.format.{MongoFormat, fromMongo, toMongo} +import io.sphere.mongo.generic.DefaultMongoFormats.given import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala index ddd9996a..a14c693b 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala @@ -2,6 +2,7 @@ package io.sphere.mongo.generic import io.sphere.mongo.MongoUtils.dbObj import io.sphere.mongo.format.{MongoFormat, fromMongo, toMongo} +import io.sphere.mongo.generic.DefaultMongoFormats.given import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala index 48fff449..bd6dffae 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala @@ -3,6 +3,7 @@ package io.sphere.mongo.generic import com.mongodb.DBObject import io.sphere.mongo.MongoUtils.dbObj import io.sphere.mongo.format.MongoFormat +import io.sphere.mongo.generic.DefaultMongoFormats.given import org.bson.BSONObject import org.scalatest.Assertion import org.scalatest.matchers.must.Matchers @@ -211,7 +212,8 @@ object SumTypesDerivingSpec { case class Custom(rgb: String) extends Color10 implicit val redFormatter: TypedMongoFormat[Red.type] = new TypedMongoFormat[Red.type] { - override def toMongoValue(a: Red.type): MongoType = dbObj("type" -> "Red", "extraField" -> "panda") + override def toMongoValue(a: Red.type): MongoType = + dbObj("type" -> "Red", "extraField" -> "panda") override def fromMongoValue(any: MongoType): Red.type = Red } val format = deriveMongoFormat[Color10] @@ -223,7 +225,7 @@ object SumTypesDerivingSpec { case class Custom(rgb: String) extends Color11 implicit val customFormatter: TypedMongoFormat[Custom] = new TypedMongoFormat[Custom] { - override def toMongoValue(a: Custom): MongoType = + override def toMongoValue(a: Custom): MongoType = dbObj("type" -> "Custom", "rgb" -> a.rgb, "extraField" -> "panda") override def fromMongoValue(any: MongoType): Custom = Custom(any.asInstanceOf[BSONObject].get("rgb").asInstanceOf[String]) @@ -241,12 +243,13 @@ object SumTypesDerivingSpec { case object Red extends ColorUpperBound case class Custom[Type1 <: Bound](rgb: String) extends ColorUpperBound - implicit def customFormatter[A <: Bound]: TypedMongoFormat[Custom[A]] = new TypedMongoFormat[Custom[A]] { - override def toMongoValue(a: Custom[A]): MongoType = - dbObj("type" -> "Custom", "rgb" -> a.rgb, "extraField" -> "panda") - override def fromMongoValue(any: MongoType): Custom[A] = - Custom(any.asInstanceOf[BSONObject].get("rgb").asInstanceOf[String]) - } + implicit def customFormatter[A <: Bound]: TypedMongoFormat[Custom[A]] = + new TypedMongoFormat[Custom[A]] { + override def toMongoValue(a: Custom[A]): MongoType = + dbObj("type" -> "Custom", "rgb" -> a.rgb, "extraField" -> "panda") + override def fromMongoValue(any: MongoType): Custom[A] = + Custom(any.asInstanceOf[BSONObject].get("rgb").asInstanceOf[String]) + } val format = deriveMongoFormat[ColorUpperBound] } From e19d164cfd190f7f9cd1d34b7a69bf0520dadec9 Mon Sep 17 00:00:00 2001 From: Marcelo Gomes Date: Thu, 23 May 2024 16:40:05 +0200 Subject: [PATCH 022/142] ENE-49 Enums work --- .gitignore | 1 + .../io/sphere/mongo/SerializationTest.scala | 128 ++++++++++++++---- .../mongo/generic/DefaultValuesSpec.scala | 14 +- 3 files changed, 108 insertions(+), 35 deletions(-) diff --git a/.gitignore b/.gitignore index f724f421..ad3def41 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ plugins-ide.sbt src_managed *.deb *.changes +*.worksheet.sc diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala index 96d4024d..a3ccfdd4 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala @@ -4,25 +4,37 @@ import com.mongodb.BasicDBObject import io.sphere.mongo.generic.TypedMongoFormat import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec +import java.util.UUID -object SerializationTest: - // For semi-automatic derivarion +object ProductTypes: + // For semi-automatic derivarion + default value argument case class Something(a: Option[Int], b: Int = 2) // For Automatic derivation with `derives` - case class Frunfles(a: Option[Int], b: Int = 2) derives TypedMongoFormat + case class Frunfles(a: Option[Int], b: Int) derives TypedMongoFormat + // Union type field - doesn't compile! + // case class Identifier(idOrKey: UUID | String) derives TypedMongoFormat +end ProductTypes + +object SumTypes: object Color extends Enumeration: val Blue, Red, Yellow = Value - sealed trait PictureSize - case object Small extends PictureSize - case object Medium extends PictureSize + sealed trait Coffee derives TypedMongoFormat + object Coffee: + case object Espresso extends Coffee + case class Other(name: String) extends Coffee -class SerializationTest extends AnyWordSpec with Matchers: - import SerializationTest.* + enum Visitor derives TypedMongoFormat: + case User(email: String, password: String) + case Anonymous +end SumTypes +class SerializationTest extends AnyWordSpec with Matchers: "mongoProduct" must { + import ProductTypes.* + "deserialize mongo object" in { val dbo = new BasicDBObject dbo.put("a", Integer.valueOf(3)) @@ -60,26 +72,86 @@ class SerializationTest extends AnyWordSpec with Matchers: TypedMongoFormat[Frunfles].fromMongoValue(serializedObject) must be(frunfles) } -// "generate a format that use default values" in { -// // TODO https://stackoverflow.com/questions/68421043/type-class-derivation-accessing-default-values -// val dbo = new BasicDBObject() -// dbo.put("a", Integer.valueOf(3)) -// -// val mongoFormat: TypedMongoFormat[Something] = io.sphere.mongo.generic.deriveMongoFormat -// val something = mongoFormat.fromMongoValue(dbo) -// something must be(Something(Some(3), 2)) -// } + "generate a format that use default values" in { + // // TODO https://stackoverflow.com/questions/68421043/type-class-derivation-accessing-default-values + // val dbo = new BasicDBObject() + // dbo.put("a", Integer.valueOf(3)) + + // val mongoFormat: TypedMongoFormat[Something] = io.sphere.mongo.generic.deriveMongoFormat + // val something = mongoFormat.fromMongoValue(dbo) + // something must be(Something(Some(3), 2)) + } } - "mongoEnum" must { -// "serialize and deserialize enums" in { -// val mongo: TypedMongoFormat[Color.Value] = io.sphere.mongo.generic.deriveMongoFormat -// -// // mongo java driver knows how to encode/decode Strings -// val serializedObject = mongo.toMongoValue(Color.Red).asInstanceOf[String] -// serializedObject must be("Red") -// -// val enumValue = mongo.fromMongoValue(serializedObject) -// enumValue must be(Color.Red) -// } + // Both sealed hierarchies and enums get this "type" field, even the Singleton cases. + // Is this what we want, or should the Singleton cases be just a String? + "mongoSum" must { + import SumTypes.* + + "serialize and deserialize sealed hierarchies" in { + val mongo = TypedMongoFormat[Coffee] + + val espressoObj = { + val dbo = new BasicDBObject + dbo.put("type", "Espresso") + dbo + } + val serializedEspresso = mongo.toMongoValue(Coffee.Espresso) + val deserializedEspresso = mongo.fromMongoValue(serializedEspresso) + serializedEspresso must be(espressoObj) + deserializedEspresso must be(Coffee.Espresso) + + val name = "Capuccino" + val capuccino = Coffee.Other(name) + val capuccinoObj = { + val dbo = new BasicDBObject + dbo.put("name", name) + dbo.put("type", "Other") + dbo + } + val serializedCapuccino = mongo.toMongoValue(capuccino) + val deserializedCapuccino = mongo.fromMongoValue(capuccinoObj) + serializedCapuccino must be(capuccinoObj) + deserializedCapuccino must be(capuccino) + } + + "serialize and deserialize enums" in { + val mongo = TypedMongoFormat[Visitor] + + val anonObj = { + val dbo = new BasicDBObject + dbo.put("type", "Anonymous") + dbo + } + val serializedAnon = mongo.toMongoValue(Visitor.Anonymous) + val deserializedAnon = mongo.fromMongoValue(serializedAnon) + serializedAnon must be(anonObj) + deserializedAnon must be(Visitor.Anonymous) + + val email = "ian@sosafe.com" + val password = "123456" + val user = Visitor.User(email, password) + val userObj = { + val dbo = new BasicDBObject + dbo.put("email", email) + dbo.put("password", password) + dbo.put("type", "User") + dbo + } + val serializedUser = mongo.toMongoValue(user) + val deserializedUser = mongo.fromMongoValue(userObj) + serializedUser must be(userObj) + deserializedUser must be(user) + } + + "serialize and deserialize enumerations" in { + // val mongo: TypedMongoFormat[Color.Value] = io.sphere.mongo.generic.deriveMongoFormat + + // // mongo java driver knows how to encode/decode Strings + // val serializedObject = mongo.toMongoValue(Color.Red).asInstanceOf[String] + // serializedObject must be("Red") + + // val enumValue = mongo.fromMongoValue(serializedObject) + // enumValue must be(Color.Red) + } } diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala index 6a5edad2..978b41f4 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala @@ -12,11 +12,11 @@ class DefaultValuesSpec extends AnyWordSpec with Matchers: "deriving MongoFormat" must { "handle default values" in { val dbo = dbObj() - val test = MongoFormat[Test].fromMongoValue(dbo) - test.value1 mustBe "hello" - test.value2 mustBe None - test.value3 mustBe None - test.value4 mustBe Some("hi") + // val test = MongoFormat[Test].fromMongoValue(dbo) + // test.value1 mustBe "hello" + // test.value2 mustBe None + // test.value3 mustBe None + // test.value4 mustBe Some("hi") } } @@ -27,5 +27,5 @@ object DefaultValuesSpec: value3: Option[String] = None, value4: Option[String] = Some("hi") ) - object Test: - implicit val mongo: MongoFormat[Test] = mongoProduct(apply _) + // object Test: + // implicit val mongo: MongoFormat[Test] = mongoProduct(apply _) From 2f5d1eb98d450e1bf98096783fbcbebe5e4f87f7 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 23 May 2024 17:26:12 +0200 Subject: [PATCH 023/142] Support default values --- .../mongo/generic/AnnotationReader.scala | 28 ++++++++++++--- .../mongo/generic/DefaultMongoFormats.scala | 35 +++++++++++++++++++ .../io/sphere/mongo/generic/Derivation.scala | 19 +++++++--- .../mongo/format/OptionMongoFormatSpec.scala | 3 +- .../mongo/generic/DefaultValuesSpec.scala | 6 ++-- .../mongo/generic/MongoEmbeddedSpec.scala | 26 +++++++------- .../mongo/generic/SumTypesDerivingSpec.scala | 1 - 7 files changed, 91 insertions(+), 27 deletions(-) create mode 100644 mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/DefaultMongoFormats.scala diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala index c56f94e2..04d273dc 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala @@ -4,7 +4,12 @@ import scala.quoted.{Expr, Quotes, Type, Varargs} private type MA = MongoAnnotation -case class Field(name: String, embedded: Boolean, ignored: Boolean, mongoKey: Option[MongoKey]) { +case class Field( + name: String, + embedded: Boolean, + ignored: Boolean, + mongoKey: Option[MongoKey], + defaultArgument: Option[Any]) { val fieldName: String = mongoKey.map(_.value).getOrElse(name) } case class CaseClassMetaData( @@ -85,7 +90,7 @@ class AnnotationReader(using q: Quotes): .filter(_.isExprOf[MongoTypeHintField]) .map(_.asExprOf[MongoTypeHintField]) - private def collectFieldInfo(s: Symbol): Expr[Field] = + private def collectFieldInfo(companion: Symbol)(s: Symbol, paramIdx: Int): Expr[Field] = val embedded = Expr(s.annotations.exists(findEmbedded)) val ignored = Expr(s.annotations.exists(findIgnored)) val name = Expr(s.name) @@ -93,11 +98,26 @@ class AnnotationReader(using q: Quotes): case Some(k) => '{ Some($k) } case None => '{ None } } - '{ Field(name = $name, embedded = $embedded, ignored = $ignored, mongoKey = $mongoKey) } + val defArgOpt = companion + .methodMember(s"$$lessinit$$greater$$default$$${paramIdx + 1}") + .headOption + .map(dm => Ref(dm).asExprOf[Any]) match { + case Some(k) => '{ Some($k) } + case None => '{ None } + } + + '{ + Field( + name = $name, + embedded = $embedded, + ignored = $ignored, + mongoKey = $mongoKey, + defaultArgument = $defArgOpt) + } private def caseClassMetaData(sym: Symbol): Expr[CaseClassMetaData] = val caseParams = sym.primaryConstructor.paramSymss.take(1).flatten - val fields = Varargs(caseParams.map(collectFieldInfo)) + val fields = Varargs(caseParams.zipWithIndex.map(collectFieldInfo(sym.companionModule))) val name = Expr(sym.name) val typeHint = sym.annotations.map(findTypeHint).find(_.isDefined).flatten match { case Some(th) => '{ Some($th) } diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/DefaultMongoFormats.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/DefaultMongoFormats.scala new file mode 100644 index 00000000..b6c16b88 --- /dev/null +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/DefaultMongoFormats.scala @@ -0,0 +1,35 @@ +package io.sphere.mongo.generic + +import com.mongodb.BasicDBObject + +object DefaultMongoFormats extends DefaultMongoFormats {} + +trait DefaultMongoFormats { + given TypedMongoFormat[Int] = new NativeMongoFormat[Int] + + given TypedMongoFormat[String] = new NativeMongoFormat[String] + + given TypedMongoFormat[Boolean] = new NativeMongoFormat[Boolean] + + given [A](using TypedMongoFormat[A]): TypedMongoFormat[Option[A]] = + new TypedMongoFormat[Option[A]]: + override def toMongoValue(a: Option[A]): MongoType = + a match + case Some(value) => summon[TypedMongoFormat[A]].toMongoValue(value) + case None => MongoNothing + + override def fromMongoValue(mongoType: MongoType): Option[A] = + val fieldNames = summon[TypedMongoFormat[A]].fieldNames + if (mongoType == null) None + else + mongoType match + case s: SimpleMongoType => Some(summon[TypedMongoFormat[A]].fromMongoValue(s)) + case bson: BasicDBObject => + val bsonFieldNames = bson.keySet().toArray + if (fieldNames.nonEmpty && bsonFieldNames.intersect(fieldNames).isEmpty) None + else Some(summon[TypedMongoFormat[A]].fromMongoValue(bson)) + case MongoNothing => None // This can't happen, but it makes the compiler happy + + override def default: Option[Option[A]] = Some(None) + +} diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala index 7cf5aae3..5d261348 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala @@ -20,6 +20,8 @@ trait TypedMongoFormat[A] extends Serializable { // /** needed JSON fields - ignored if empty */ val fieldNames: Vector[String] = TypedMongoFormat.emptyFieldsSet + + def default: Option[A] = None } private final class NativeMongoFormat[A <: SimpleMongoType] extends TypedMongoFormat[A] { @@ -109,10 +111,19 @@ object TypedMongoFormat: case bson: BasicDBObject => val fieldsAsAList = fieldsAndFormatters .map { (field, format) => - if (field.embedded) - format.fromMongoValue(bson) - else - format.fromMongoValue(bson.get(field.fieldName).asInstanceOf[MongoType]) + if (field.embedded) format.fromMongoValue(bson) + else { + val value = bson.get(field.fieldName) + val defaultValue = field.defaultArgument.orElse(format.default) + + if (value != null) format.fromMongoValue(value.asInstanceOf[MongoType]) + else + defaultValue match + case Some(value) => value + case None => + throw new Exception( + s"Missing required field '${field.fieldName}' on deserialization.") + } } val tuple = Tuple.fromArray(fieldsAsAList.toArray) mirrorOfProduct.fromTuple(tuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala index ee435285..2e4688d0 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala @@ -48,8 +48,7 @@ class OptionMongoFormatSpec extends AnyWordSpec with Matchers with OptionValues result.value.value2 mustEqual 45 } - "handle presence of not all the fields" in pendingUntilFixed { - // TODO we need to implement default value handling to fix this + "handle presence of not all the fields" in { val dbo = dbObj("value1" -> "a") an[Exception] mustBe thrownBy(TypedMongoFormat[Option[SimpleClass]].fromMongoValue(dbo)) } diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala index 6a5edad2..660cc202 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala @@ -1,7 +1,7 @@ package io.sphere.mongo.generic import io.sphere.mongo.MongoUtils.* -import io.sphere.mongo.format.DefaultMongoFormats.* +import io.sphere.mongo.generic.DefaultMongoFormats.given import io.sphere.mongo.format.MongoFormat import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -12,7 +12,7 @@ class DefaultValuesSpec extends AnyWordSpec with Matchers: "deriving MongoFormat" must { "handle default values" in { val dbo = dbObj() - val test = MongoFormat[Test].fromMongoValue(dbo) + val test = TypedMongoFormat[Test].fromMongoValue(dbo) test.value1 mustBe "hello" test.value2 mustBe None test.value3 mustBe None @@ -28,4 +28,4 @@ object DefaultValuesSpec: value4: Option[String] = Some("hi") ) object Test: - implicit val mongo: MongoFormat[Test] = mongoProduct(apply _) + implicit val mongo: TypedMongoFormat[Test] = deriveMongoFormat diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala index e264acca..c294bd7e 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala @@ -43,7 +43,7 @@ class MongoEmbeddedSpec extends AnyWordSpec with Matchers with OptionValues { result mustEqual dbo } - "validate that the db object contains all needed fields" in pendingUntilFixed { + "validate that the db object contains all needed fields" in { // TODO default field val dbo = dbObj( "name" -> "ze name", @@ -80,17 +80,17 @@ class MongoEmbeddedSpec extends AnyWordSpec with Matchers with OptionValues { test2.embedded.value.value2 mustEqual 45 } - "ignore ignored fields" in pendingUntilFixed { - // TODO Ignore - val dbo = dbObj( - "value1" -> "ze value1", - "_value2" -> 45 - ) - val test3 = deriveMongoFormat[Test3].fromMongoValue(dbo) - test3.name mustEqual "default" - test3.embedded.value.value1 mustEqual "ze value1" - test3.embedded.value.value2 mustEqual 45 - } +// "ignore ignored fields" in pendingUntilFixed { +// // TODO Ignore +// val dbo = dbObj( +// "value1" -> "ze value1", +// "_value2" -> 45 +// ) +// val test3 = deriveMongoFormat[Test3].fromMongoValue(dbo) +// test3.name mustEqual "default" +// test3.embedded.value.value1 mustEqual "ze value1" +// test3.embedded.value.value2 mustEqual 45 +// } "check for sub-fields" in { val dbo = dbObj( @@ -113,7 +113,7 @@ class MongoEmbeddedSpec extends AnyWordSpec with Matchers with OptionValues { test2.embedded mustEqual None } - "validate the absence of some embedded attributes" in pendingUntilFixed { + "validate the absence of some embedded attributes" in { val dbo = dbObj( "name" -> "ze name", "value1" -> "ze value1" diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala index bd6dffae..b639260a 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala @@ -2,7 +2,6 @@ package io.sphere.mongo.generic import com.mongodb.DBObject import io.sphere.mongo.MongoUtils.dbObj -import io.sphere.mongo.format.MongoFormat import io.sphere.mongo.generic.DefaultMongoFormats.given import org.bson.BSONObject import org.scalatest.Assertion From e2b478accf0c1652c97e3c426f1af8066afe4200 Mon Sep 17 00:00:00 2001 From: Marcelo Gomes Date: Thu, 23 May 2024 18:28:04 +0200 Subject: [PATCH 024/142] ENE-49 Spec for class with fields with default value --- .../io/sphere/mongo/SerializationTest.scala | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala index bfda1a0e..82149f1e 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala @@ -73,14 +73,36 @@ class SerializationTest extends AnyWordSpec with Matchers: TypedMongoFormat[Frunfles].fromMongoValue(serializedObject) must be(frunfles) } + // https://stackoverflow.com/questions/68421043/type-class-derivation-accessing-default-values "generate a format that use default values" in { - // // TODO https://stackoverflow.com/questions/68421043/type-class-derivation-accessing-default-values - // val dbo = new BasicDBObject() - // dbo.put("a", Integer.valueOf(3)) + val sthObj1 = { + val dbo = new BasicDBObject() + dbo.put("a", Integer.valueOf(3)) + dbo + } + val s1 = TypedMongoFormat[Something].fromMongoValue(sthObj1) + s1 must be(Something(a = Some(3), b = 2)) + + val sthObj2 = new BasicDBObject() // an empty object + val s2 = TypedMongoFormat[Something].fromMongoValue(sthObj2) + s2 must be(Something(a = None, b = 2)) + + val sthObj3 = { + val dbo = new BasicDBObject() + dbo.put("b", Integer.valueOf(33)) + dbo + } + val s3 = TypedMongoFormat[Something].fromMongoValue(sthObj3) + s3 must be(Something(a = None, b = 33)) - // val mongoFormat: TypedMongoFormat[Something] = io.sphere.mongo.generic.deriveMongoFormat - // val something = mongoFormat.fromMongoValue(dbo) - // something must be(Something(Some(3), 2)) + val sthObj4 = { + val dbo = new BasicDBObject() + dbo.put("a", Integer.valueOf(33)) + dbo.put("b", Integer.valueOf(44)) + dbo + } + val s4 = TypedMongoFormat[Something].fromMongoValue(sthObj4) + s4 must be(Something(a = Some(33), b = 44)) } } From aecda6eaa89590934f6b917df070b4ed5746cce2 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 23 May 2024 21:15:29 +0200 Subject: [PATCH 025/142] Add DeriveMongoFormatSpec --- .../main/scala/io/sphere/mongo/format.scala | 10 ++ .../mongo/generic/DeriveMongoFormatSpec.scala | 136 ++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/format.scala create mode 100644 mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/format.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/format.scala new file mode 100644 index 00000000..a16d7771 --- /dev/null +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/format.scala @@ -0,0 +1,10 @@ +package io.sphere.mongo + +import io.sphere.mongo.generic +import io.sphere.mongo.generic.{MongoType, TypedMongoFormat} + +def toMongo[A: TypedMongoFormat](a: A): MongoType = + summon[generic.TypedMongoFormat[A]].toMongoValue(a) + +def fromMongo[A: TypedMongoFormat](any: MongoType): A = + summon[generic.TypedMongoFormat[A]].fromMongoValue(any) diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala new file mode 100644 index 00000000..6accfd78 --- /dev/null +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala @@ -0,0 +1,136 @@ +package io.sphere.mongo.generic + +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec +import io.sphere.mongo.generic.DefaultMongoFormats.given +import io.sphere.mongo.MongoUtils.* +import io.sphere.mongo.{fromMongo, toMongo} + +class DeriveMongoFormatSpec extends AnyWordSpec with Matchers { + import DeriveMongoFormatSpec.given + import DeriveMongoFormatSpec.* + + "deriving MongoFormat" must { + "read normal singleton values" in { + val user = fromMongo[UserWithPicture]( + dbObj( + "userId" -> "foo-123", + "pictureSize" -> dbObj("type" -> "Medium"), + "pictureUrl" -> "http://example.com")) + + user must be(UserWithPicture("foo-123", Medium, "http://example.com")) + } + + "read custom singleton values" in { + val user = fromMongo[UserWithPicture]( + dbObj( + "userId" -> "foo-123", + "pictureSize" -> dbObj("type" -> "bar", "width" -> 23, "height" -> 30), + "pictureUrl" -> "http://example.com")) + + user must be(UserWithPicture("foo-123", Custom(23, 30), "http://example.com")) + } + + "fail to read if singleton value is unknown" in { + a[Exception] must be thrownBy fromMongo[UserWithPicture]( + dbObj( + "userId" -> "foo-123", + "pictureSize" -> dbObj("type" -> "Unknown"), + "pictureUrl" -> "http://example.com")) + } + + "write normal singleton values" in { + val dbo = toMongo[UserWithPicture](UserWithPicture("foo-123", Medium, "http://example.com")) + dbo must be( + dbObj( + "userId" -> "foo-123", + "pictureSize" -> dbObj("type" -> "Medium"), + "pictureUrl" -> "http://example.com")) + } + + "write custom singleton values" in { + val dbo = + toMongo[UserWithPicture](UserWithPicture("foo-123", Custom(23, 30), "http://example.com")) + dbo must be( + dbObj( + "userId" -> "foo-123", + "pictureSize" -> dbObj("type" -> "bar", "width" -> 23, "height" -> 30), + "pictureUrl" -> "http://example.com")) + } + + "write and consequently read, which must produce the original value" in { + val originalUser = UserWithPicture("foo-123", Medium, "http://exmple.com") + val newUser = fromMongo[UserWithPicture](toMongo[UserWithPicture](originalUser)) + + newUser must be(originalUser) + } + + "read and write sealed trait with only one subtype" in { + val dbo = dbObj( + "userId" -> "foo-123", + "pictureSize" -> dbObj("type" -> "Medium"), + "pictureUrl" -> "http://example.com", + "access" -> dbObj("type" -> "Authorized", "project" -> "internal") + ) + val user = fromMongo[UserWithPicture](dbo) + + user must be( + UserWithPicture( + "foo-123", + Medium, + "http://example.com", + Some(Access.Authorized("internal")))) + val newDbo = toMongo[UserWithPicture](user) + newDbo must be(dbo) + + val newUser = fromMongo[UserWithPicture](newDbo) + newUser must be(user) + } + + "fail to derive if trait is not sealed" in { + // Sealed + "implicit val mongo: TypedMongoFormat[SealedSub] = deriveMongoFormat[SealedSub]" must compile + // Not sealed + "implicit val mongo: TypedMongoFormat[NotSealed] = deriveMongoFormat[NotSealed]" mustNot compile + // Sealed, but child is not sealed + "implicit val mongo: TypedMongoFormat[SealedParent] = deriveMongoFormat[SealedParent]" mustNot compile + } + } +} + +object DeriveMongoFormatSpec { + sealed trait PictureSize + case object Small extends PictureSize + case object Medium extends PictureSize + case object Big extends PictureSize + @MongoTypeHint(value = "bar") + case class Custom(width: Int, height: Int) extends PictureSize + + given TypedMongoFormat[PictureSize] = deriveMongoFormat[PictureSize] + + sealed trait Access + object Access { + // only one sub-type + case class Authorized(project: String) extends Access + + } + given TypedMongoFormat[Access] = deriveMongoFormat + + case class UserWithPicture( + userId: String, + pictureSize: PictureSize, + pictureUrl: String, + access: Option[Access] = None) + + given TypedMongoFormat[UserWithPicture] = deriveMongoFormat + + sealed trait SealedParent + + sealed trait SealedSub extends SealedParent + case class Sub1(x: String) extends SealedSub + case class Sub2(y: Int) extends SealedSub + + trait NotSealed extends SealedParent + case class Sub3(x: String) extends NotSealed + case class Sub4(y: Int) extends NotSealed +} From 67943d767c0ba936cca2ecd43ab0ba8542d6a076 Mon Sep 17 00:00:00 2001 From: Peter Empen Date: Fri, 24 May 2024 11:58:35 +0200 Subject: [PATCH 026/142] brushing up some code --- .../io/sphere/mongo/generic/Derivation.scala | 33 +++++++++---------- .../mongo/generic/DefaultValuesSpec.scala | 27 +++++++-------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala index 5d261348..328d1af2 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala @@ -56,16 +56,15 @@ object TypedMongoFormat: inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): TypedMongoFormat[A] = new TypedMongoFormat[A]: - val traitMetaData = AnnotationReader.readTraitMetaData[A] - val typeHintMap = traitMetaData.subtypes.collect { + private val traitMetaData = AnnotationReader.readTraitMetaData[A] + private val typeHintMap = traitMetaData.subtypes.collect { case (name, classMeta) if classMeta.typeHint.isDefined => name -> classMeta.typeHint.get } - val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) - val formatters = summonFormatters[mirrorOfSum.MirroredElemTypes] - val names = constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector - .asInstanceOf[Vector[String]] - val formattersByTypeName = names.zip(formatters).toMap + private val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) + private val formatters = summonFormatters[mirrorOfSum.MirroredElemTypes] + private val names = constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector.asInstanceOf[Vector[String]] + private val formattersByTypeName = names.zip(formatters).toMap override def toMongoValue(a: A): MongoType = // we never get a trait here, only classes, it's safe to assume Product @@ -89,9 +88,9 @@ object TypedMongoFormat: inline private def deriveCaseClass[A]( mirrorOfProduct: Mirror.ProductOf[A]): TypedMongoFormat[A] = new TypedMongoFormat[A]: - val caseClassMetaData = AnnotationReader.readCaseClassMetaData[A] - val formatters = summonFormatters[mirrorOfProduct.MirroredElemTypes] - val fieldsAndFormatters = caseClassMetaData.fields.zip(formatters) + private val caseClassMetaData = AnnotationReader.readCaseClassMetaData[A] + private val formatters = summonFormatters[mirrorOfProduct.MirroredElemTypes] + private val fieldsAndFormatters = caseClassMetaData.fields.zip(formatters) override val fieldNames: Vector[String] = fieldsAndFormatters.flatMap((field, formatter) => if (field.embedded) formatter.fieldNames :+ field.name @@ -109,26 +108,26 @@ object TypedMongoFormat: override def fromMongoValue(mongoType: MongoType): A = mongoType match case bson: BasicDBObject => - val fieldsAsAList = fieldsAndFormatters + val fields = fieldsAndFormatters .map { (field, format) => if (field.embedded) format.fromMongoValue(bson) else { val value = bson.get(field.fieldName) - val defaultValue = field.defaultArgument.orElse(format.default) - - if (value != null) format.fromMongoValue(value.asInstanceOf[MongoType]) - else + if (value ne null) format.fromMongoValue(value.asInstanceOf[MongoType]) + else { + val defaultValue = field.defaultArgument.orElse(format.default) defaultValue match case Some(value) => value case None => throw new Exception( s"Missing required field '${field.fieldName}' on deserialization.") + } } } - val tuple = Tuple.fromArray(fieldsAsAList.toArray) + val tuple = Tuple.fromArray(fields.toArray) mirrorOfProduct.fromTuple(tuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) - case x => throw new Exception(s"BsonObject is expected for a class, instead got: ${x}") + case x => throw new Exception(s"BasicDBObject is expected for a class, instead got: $x") end deriveCaseClass inline private def summonFormatters[T <: Tuple]: Vector[TypedMongoFormat[Any]] = diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala index 660cc202..269605e1 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala @@ -9,23 +9,24 @@ import org.scalatest.wordspec.AnyWordSpec class DefaultValuesSpec extends AnyWordSpec with Matchers: import DefaultValuesSpec.* - "deriving MongoFormat" must { + "deriving TypedMongoFormat" must { "handle default values" in { val dbo = dbObj() - val test = TypedMongoFormat[Test].fromMongoValue(dbo) - test.value1 mustBe "hello" - test.value2 mustBe None - test.value3 mustBe None - test.value4 mustBe Some("hi") + val test = TypedMongoFormat[CaseClass].fromMongoValue(dbo) + import test._ + field1 mustBe "hello" + field2 mustBe None + field3 mustBe None + field4 mustBe Some("hi") } } object DefaultValuesSpec: - case class Test( - value1: String = "hello", - value2: Option[String], - value3: Option[String] = None, - value4: Option[String] = Some("hi") + private case class CaseClass( + field1: String = "hello", + field2: Option[String], + field3: Option[String] = None, + field4: Option[String] = Some("hi") ) - object Test: - implicit val mongo: TypedMongoFormat[Test] = deriveMongoFormat + private object CaseClass: + implicit val mongo: TypedMongoFormat[CaseClass] = deriveMongoFormat From ba4836757e555cc9cb5d47ba62248ec1d6d784d4 Mon Sep 17 00:00:00 2001 From: Peter Empen Date: Fri, 24 May 2024 16:42:02 +0200 Subject: [PATCH 027/142] stat with MongoIgnore --- .../io/sphere/mongo/generic/Derivation.scala | 27 ++++++++------ .../mongo/generic/MongoEmbeddedSpec.scala | 37 +++++++++---------- .../mongo/generic/MongoIgnoreSpec.scala | 30 +++++++++++++++ 3 files changed, 63 insertions(+), 31 deletions(-) create mode 100644 mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoIgnoreSpec.scala diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala index 328d1af2..fb08450a 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala @@ -63,7 +63,8 @@ object TypedMongoFormat: } private val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) private val formatters = summonFormatters[mirrorOfSum.MirroredElemTypes] - private val names = constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector.asInstanceOf[Vector[String]] + private val names = constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector + .asInstanceOf[Vector[String]] private val formattersByTypeName = names.zip(formatters).toMap override def toMongoValue(a: A): MongoType = @@ -109,19 +110,21 @@ object TypedMongoFormat: mongoType match case bson: BasicDBObject => val fields = fieldsAndFormatters - .map { (field, format) => - if (field.embedded) format.fromMongoValue(bson) + .map { case (Field(name, embedded, ignored, mongoKey, defaultArgument), format) => + def defaultValue = defaultArgument.orElse(format.default) + if (ignored) + default.getOrElse { + throw new Exception( + s"Missing default parameter value for ignored field `$name` on deserialization.") + } + else if (embedded) format.fromMongoValue(bson) else { - val value = bson.get(field.fieldName) + val value = bson.get(name) if (value ne null) format.fromMongoValue(value.asInstanceOf[MongoType]) - else { - val defaultValue = field.defaultArgument.orElse(format.default) - defaultValue match - case Some(value) => value - case None => - throw new Exception( - s"Missing required field '${field.fieldName}' on deserialization.") - } + else + defaultValue.getOrElse { + throw new Exception(s"Missing required field '$name' on deserialization.") + } } } val tuple = Tuple.fromArray(fields.toArray) diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala index c294bd7e..9c1b1779 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala @@ -9,19 +9,19 @@ import org.scalatest.wordspec.AnyWordSpec import scala.util.Try object MongoEmbeddedSpec { - case class Embedded(value1: String, @MongoKey("_value2") value2: Int) + private case class Embedded(value1: String, @MongoKey("_value2") value2: Int) - case class Test1(name: String, @MongoEmbedded embedded: Embedded) + private case class Test1(name: String, @MongoEmbedded embedded: Embedded) - case class Test2(name: String, @MongoEmbedded embedded: Option[Embedded] = None) + private case class Test2(name: String, @MongoEmbedded embedded: Option[Embedded] = None) - case class Test3( + private case class ClassWithMongoIgnore( @MongoIgnore name: String = "default", @MongoEmbedded embedded: Option[Embedded] = None) - case class SubTest4(@MongoEmbedded embedded: Embedded) + private case class SubTest4(@MongoEmbedded embedded: Embedded) - case class Test4(subField: Option[SubTest4] = None) + private case class Test4(subField: Option[SubTest4] = None) } class MongoEmbeddedSpec extends AnyWordSpec with Matchers with OptionValues { @@ -80,17 +80,16 @@ class MongoEmbeddedSpec extends AnyWordSpec with Matchers with OptionValues { test2.embedded.value.value2 mustEqual 45 } -// "ignore ignored fields" in pendingUntilFixed { -// // TODO Ignore -// val dbo = dbObj( -// "value1" -> "ze value1", -// "_value2" -> 45 -// ) -// val test3 = deriveMongoFormat[Test3].fromMongoValue(dbo) -// test3.name mustEqual "default" -// test3.embedded.value.value1 mustEqual "ze value1" -// test3.embedded.value.value2 mustEqual 45 -// } + "ignore ignored fields" in { + val dbo = dbObj( + "value1" -> "ze value1", + "_value2" -> 45 + ) + val test3 = deriveMongoFormat[ClassWithMongoIgnore].fromMongoValue(dbo) + test3.name mustEqual "default" + test3.embedded.value.value1 mustEqual "ze value1" + test3.embedded.value.value2 mustEqual 45 + } "check for sub-fields" in { val dbo = dbObj( @@ -109,8 +108,8 @@ class MongoEmbeddedSpec extends AnyWordSpec with Matchers with OptionValues { "name" -> "ze name" ) val test2 = deriveMongoFormat[Test2].fromMongoValue(dbo) - test2.name mustEqual "ze name" - test2.embedded mustEqual None + test2.name mustBe "ze name" + test2.embedded mustBe None } "validate the absence of some embedded attributes" in { diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoIgnoreSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoIgnoreSpec.scala new file mode 100644 index 00000000..08e80b71 --- /dev/null +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoIgnoreSpec.scala @@ -0,0 +1,30 @@ +package io.sphere.mongo.generic + +import io.sphere.mongo.MongoUtils.* +import io.sphere.mongo.generic.DefaultMongoFormats.given +import org.scalatest.OptionValues +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import scala.util.chaining.* + +object MongoIgnoreSpec { + private val dbo = dbObj( + "name" -> "aName" + ) + + private case class MissingDefault(name: String, @MongoIgnore age: Int) +} + +class MongoIgnoreSpec extends AnyWordSpec with Matchers with OptionValues { + import MongoIgnoreSpec.* + + "MongoIgnore" when { + "annotated field has no default" must { + "fail with a suitable message" in { + val e = the[Exception] thrownBy deriveMongoFormat[MissingDefault].fromMongoValue(dbo) + e.getMessage mustBe "Missing default parameter value for ignored field `age` on deserialization." + } + } + } +} From 1b85335043d23fb95f8e5a4f46cb7ecaa6ac1454 Mon Sep 17 00:00:00 2001 From: Yehia AboSedira Date: Fri, 24 May 2024 16:49:00 +0200 Subject: [PATCH 028/142] ENE-49 Porting Json Lib - Initial version --- build.sbt | 6 + .../json/generic/AnnotationReader.scala | 137 ++++++ .../io/sphere/json/generic/Annotations.scala | 11 + .../io/sphere/json/generic/Derivation.scala | 132 ++++++ .../sphere/json/DeriveSingletonJSONSpec.scala | 144 +++++++ .../io/sphere/json/ForProductNSpec.scala | 35 ++ .../io/sphere/json/JSONEmbeddedSpec.scala | 131 ++++++ .../test/scala/io/sphere/json/JSONSpec.scala | 395 ++++++++++++++++++ .../io/sphere/json/NullHandlingSpec.scala | 68 +++ .../io/sphere/json/OptionReaderSpec.scala | 150 +++++++ .../io/sphere/json/TypesSwitchSpec.scala | 87 ++++ .../json/generic/DefaultValuesSpec.scala | 44 ++ .../io/sphere/json/generic/JSONKeySpec.scala | 47 +++ .../json/generic/JsonTypeHintFieldSpec.scala | 69 +++ 14 files changed, 1456 insertions(+) create mode 100644 json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala create mode 100644 json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Annotations.scala create mode 100644 json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Derivation.scala create mode 100644 json/json-derivation-scala-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala create mode 100644 json/json-derivation-scala-3/src/test/scala/io/sphere/json/ForProductNSpec.scala create mode 100644 json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala create mode 100644 json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala create mode 100644 json/json-derivation-scala-3/src/test/scala/io/sphere/json/NullHandlingSpec.scala create mode 100644 json/json-derivation-scala-3/src/test/scala/io/sphere/json/OptionReaderSpec.scala create mode 100644 json/json-derivation-scala-3/src/test/scala/io/sphere/json/TypesSwitchSpec.scala create mode 100644 json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala create mode 100644 json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JSONKeySpec.scala create mode 100644 json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala diff --git a/build.sbt b/build.sbt index ae5af1a0..bdd207d5 100644 --- a/build.sbt +++ b/build.sbt @@ -101,6 +101,12 @@ lazy val `sphere-json-derivation` = project .settings(Fmpp.settings: _*) .dependsOn(`sphere-json-core`) +lazy val `sphere-json-derivation-scala-3` = project + .settings(crossScalaVersions := Seq(scala3)) + .in(file("./json/json-derivation-scala-3")) + .settings(standardSettings: _*) + .dependsOn(`sphere-json-core`) + lazy val `sphere-json` = project .in(file("./json")) .settings(standardSettings: _*) diff --git a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala new file mode 100644 index 00000000..c2802599 --- /dev/null +++ b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala @@ -0,0 +1,137 @@ +package io.sphere.json.generic + +import io.sphere.json.generic.JSONAnnotation +import io.sphere.json.generic.JSONTypeHint + +import scala.quoted.{Expr, Quotes, Type, Varargs} + +private type MA = JSONAnnotation + +case class Field( + name: String, + embedded: Boolean, + ignored: Boolean, + jsonKey: Option[JSONKey], + defaultArgument: Option[Any]) { + val fieldName: String = jsonKey.map(_.value).getOrElse(name) +} + +case class CaseClassMetaData( + name: String, + typeHintRaw: Option[JSONTypeHint], + fields: Vector[Field] +) { + val typeHint: Option[String] = + typeHintRaw.map(_.value).filterNot(_.toList.forall(_ == ' ')) +} + +case class TraitMetaData( + top: CaseClassMetaData, + typeHintFieldRaw: Option[JSONTypeHintField], + subtypes: Map[String, CaseClassMetaData] +) { + val typeDiscriminator: String = typeHintFieldRaw.map(_.value).getOrElse("type") +} + +class AnnotationReader(using q: Quotes): + import q.reflect.* + + def readCaseClassMetaData[T: Type]: Expr[CaseClassMetaData] = { + val sym = TypeRepr.of[T].typeSymbol + caseClassMetaData(sym) + } + + def readTraitMetaData[T: Type]: Expr[TraitMetaData] = + val sym = TypeRepr.of[T].typeSymbol + val typeHintField = + sym.annotations.map(findJSONTypeHintField).find(_.isDefined).flatten match { + case Some(thf) => '{ Some($thf) } + case None => '{ None } + } + + '{ + TraitMetaData( + top = ${ caseClassMetaData(sym) }, + typeHintFieldRaw = $typeHintField, + subtypes = ${ subtypeAnnotations(sym) } + ) + } + + private def annotationTree(tree: Tree): Option[Expr[MA]] = + Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[MA]).map(_.asExprOf[MA]) + + private def findEmbedded(tree: Tree): Boolean = + Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[JSONEmbedded]).isDefined + + private def findIgnored(tree: Tree): Boolean = + Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[JSONIgnore]).isDefined + + private def findKey(tree: Tree): Option[Expr[JSONKey]] = + Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[JSONKey]).map(_.asExprOf[JSONKey]) + + private def findTypeHint(tree: Tree): Option[Expr[JSONTypeHint]] = + Option + .when(tree.isExpr)(tree.asExpr) + .filter(_.isExprOf[JSONTypeHint]) + .map(_.asExprOf[JSONTypeHint]) + + private def findJSONTypeHintField(tree: Tree): Option[Expr[JSONTypeHintField]] = + Option + .when(tree.isExpr)(tree.asExpr) + .filter(_.isExprOf[JSONTypeHintField]) + .map(_.asExprOf[JSONTypeHintField]) + + private def collectFieldInfo(companion: Symbol)(s: Symbol, paramIdx: Int): Expr[Field] = + val embedded = Expr(s.annotations.exists(findEmbedded)) + val ignored = Expr(s.annotations.exists(findIgnored)) + val name = Expr(s.name) + val key = s.annotations.map(findKey).find(_.isDefined).flatten match { + case Some(k) => '{ Some($k) } + case None => '{ None } + } + val defArgOpt = companion + .methodMember(s"$$lessinit$$greater$$default$$${paramIdx + 1}") + .headOption + .map(dm => Ref(dm).asExprOf[Any]) match { + case Some(k) => '{ Some($k) } + case None => '{ None } + } + + '{ + Field( + name = $name, + embedded = $embedded, + ignored = $ignored, + jsonKey = $key, + defaultArgument = $defArgOpt) + } + + private def caseClassMetaData(sym: Symbol): Expr[CaseClassMetaData] = + val caseParams = sym.primaryConstructor.paramSymss.take(1).flatten + val fields = Varargs(caseParams.zipWithIndex.map(collectFieldInfo(sym.companionModule))) + val name = Expr(sym.name) + val typeHint = sym.annotations.map(findTypeHint).find(_.isDefined).flatten match { + case Some(th) => '{ Some($th) } + case None => '{ None } + } + + '{ + CaseClassMetaData( + name = $name, + typeHintRaw = $typeHint, + fields = Vector($fields*) + ) + } + end caseClassMetaData + + private def subtypeAnnotation(sym: Symbol): Expr[(String, CaseClassMetaData)] = + val name = Expr(sym.name) + val annots = caseClassMetaData(sym) + '{ ($name, $annots) } + end subtypeAnnotation + + private def subtypeAnnotations(sym: Symbol): Expr[Map[String, CaseClassMetaData]] = + val subtypes = Varargs(sym.children.map(subtypeAnnotation)) + '{ Map($subtypes*) } + +end AnnotationReader diff --git a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Annotations.scala b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Annotations.scala new file mode 100644 index 00000000..7d3ace8d --- /dev/null +++ b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Annotations.scala @@ -0,0 +1,11 @@ +package io.sphere.json.generic + +import scala.annotation.StaticAnnotation + +sealed trait JSONAnnotation extends StaticAnnotation + +case class JSONEmbedded() extends JSONAnnotation +case class JSONIgnore() extends JSONAnnotation +case class JSONKey(value: String) extends JSONAnnotation +case class JSONTypeHintField(value: String) extends JSONAnnotation +case class JSONTypeHint(value: String) extends JSONAnnotation diff --git a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Derivation.scala b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Derivation.scala new file mode 100644 index 00000000..9d08905d --- /dev/null +++ b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Derivation.scala @@ -0,0 +1,132 @@ +package io.sphere.json.generic + +import cats.data.Validated +import cats.implicits.* +import io.sphere.json.{JSON, JSONParseError, JValidation} +import org.json4s.DefaultJsonFormats.given +import org.json4s.JsonAST.JValue +import org.json4s.{DefaultJsonFormats, JNull, JObject, JString, jvalue2monadic, jvalue2readerSyntax} + +import scala.deriving.Mirror +import scala.quoted.* + +inline def deriveJSON[A](using Mirror.Of[A]): JSON[A] = JSON.derived + +object JSON: + private val emptyFieldsSet: Vector[String] = Vector.empty + + inline def apply[A: JSON]: JSON[A] = summon[JSON[A]] + inline given derived[A](using Mirror.Of[A]): JSON[A] = Derivation.derived[A] + private inline def readCaseClassMetaData[T]: CaseClassMetaData = ${ readCaseClassMetaDataImpl[T] } + private inline def readTraitMetaData[T]: TraitMetaData = ${ readTraitMetaDataImpl[T] } + + private def readCaseClassMetaDataImpl[T: Type](using Quotes): Expr[CaseClassMetaData] = + AnnotationReader().readCaseClassMetaData[T] + + private def readTraitMetaDataImpl[T: Type](using Quotes): Expr[TraitMetaData] = + AnnotationReader().readTraitMetaData[T] + + private def addField(jObject: JObject, field: Field, jValue: JValue): JValue = + jValue match + case o: JObject => + if (field.embedded) JObject(jObject.obj ++ o.obj) + else JObject(jObject.obj :+ (field.fieldName -> o)) + case other => JObject(jObject.obj :+ (field.fieldName -> other)) + + private object Derivation: + + import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} + + inline def derived[A](using m: Mirror.Of[A]): JSON[A] = + inline m match + case s: Mirror.SumOf[A] => deriveTrait(s) + case p: Mirror.ProductOf[A] => deriveCaseClass(p) + + inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): JSON[A] = + new JSON[A]: + private val traitMetaData: TraitMetaData = readTraitMetaData[A] + private val typeHintMap: Map[String, String] = traitMetaData.subtypes.collect { + case (name, classMeta) if classMeta.typeHint.isDefined => + name -> classMeta.typeHint.get + } + private val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) + private val jsons: Seq[JSON[Any]] = summonFormatters[mirrorOfSum.MirroredElemTypes] + private val names: Seq[String] = + constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector + .asInstanceOf[Vector[String]] + private val jsonsByNames: Map[String, JSON[Any]] = names.zip(jsons).toMap + + override def read(jValue: JValue): JValidation[A] = + jValue match + case jObject: JObject => + val typeName = (jObject \ traitMetaData.typeDiscriminator).as[String] + val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) + val parsed = jsonsByNames(originalTypeName).read(jObject) + parsed.map(_.asInstanceOf[A]) + case x => + Validated.invalidNel( + JSONParseError(s"JSON object expected. >>> trait >>> $jValue >>> ${traitMetaData}")) + + override def write(value: A): JValue = + // we never get a trait here, only classes, it's safe to assume Product + val originalTypeName = value.asInstanceOf[Product].productPrefix + val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) + val json = jsonsByNames(originalTypeName).write(value) + json ++ JObject(traitMetaData.typeDiscriminator -> JString(typeName)) + + end deriveTrait + + inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): JSON[A] = + new JSON[A]: + private val caseClassMetaData: CaseClassMetaData = readCaseClassMetaData[A] + private val jsons: Vector[JSON[Any]] = summonFormatters[mirrorOfProduct.MirroredElemTypes] + private val fieldsAndJsons: Vector[(Field, JSON[Any])] = caseClassMetaData.fields.zip(jsons) + + private val fieldNames: Vector[String] = fieldsAndJsons.flatMap { (field, json) => + if (field.embedded) json.fields.toVector :+ field.name + else Vector(field.name) + } + + override def write(value: A): JValue = + val caseClassFields = value.asInstanceOf[Product].productIterator + jsons + .zip(caseClassFields) + .zip(caseClassMetaData.fields) + .foldLeft[JValue](JObject()) { case (jObject, ((json, fieldValue), field)) => + addField(jObject.asInstanceOf[JObject], field, json.write(fieldValue)) + } + + override def read(jValue: JValue): JValidation[A] = + jValue match + case jObject: JObject => + for { + fieldsAsAList <- fieldsAndJsons + .map((field, format) => readField(field, format, jObject)) + .sequence + fieldsAsTuple = Tuple.fromArray(fieldsAsAList.toArray) + + } yield mirrorOfProduct.fromTuple( + fieldsAsTuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) + + case x => + Validated.invalidNel(JSONParseError(s"JSON object expected. ${x}")) + + private def readField(field: Field, json: JSON[Any], jObject: JObject): JValidation[Any] = + if (field.embedded) json.read(jObject) + else if (jObject.values.contains(field.fieldName) && (jObject \ field.fieldName) != JNull) + json.read(jObject \ field.fieldName) + else + field.defaultArgument match + case Some(value) => Validated.valid(value) + case None => + Validated.invalidNel( + JSONParseError( + s"Missing required field '${field.fieldName}' on deserialization.")) + end deriveCaseClass + + inline private def summonFormatters[T <: Tuple]: Vector[JSON[Any]] = + inline erasedValue[T] match + case _: EmptyTuple => Vector.empty + case _: (t *: ts) => + summonInline[JSON[t]] + .asInstanceOf[JSON[Any]] +: summonFormatters[ts] diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala new file mode 100644 index 00000000..99504458 --- /dev/null +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala @@ -0,0 +1,144 @@ +//package io.sphere.json +//import io.sphere.json.generic.* +//import io.sphere.json.generic.JSON.derived +//import org.json4s.JValue +//import org.scalatest.matchers.must.Matchers +//import org.scalatest.wordspec.AnyWordSpec +// +//class DeriveSingletonJSONSpec extends AnyWordSpec with Matchers { +// "DeriveSingletonJSON" must { +// "read normal singleton values" in { +// val user = getFromJSON[UserWithPicture](""" +// { +// "userId": "foo-123", +// "pictureSize": "Medium", +// "pictureUrl": "http://exmple.com" +// } +// """) +// +// user must be(UserWithPicture("foo-123", Medium, "http://exmple.com")) +// } +// +// "fail to read if singleton value is unknown" in { +// a[JSONException] must be thrownBy getFromJSON[UserWithPicture](""" +// { +// "userId": "foo-123", +// "pictureSize": "foo", +// "pictureUrl": "http://exmple.com" +// } +// """) +// } +// "write normal singleton values" in { +// val userJson = toJValue(UserWithPicture("foo-123", Medium, "http://exmple.com")) +// +// val Valid(expectedJson) = parseJSON(""" +// { +// "userId": "foo-123", +// "pictureSize": "Medium", +// "pictureUrl": "http://exmple.com" +// } +// """) +// +// filter(userJson) must be(expectedJson) +// } +// +// "read custom singleton values" in { +// val user = getFromJSON[UserWithPicture](""" +// { +// "userId": "foo-123", +// "pictureSize": "bar", +// "pictureUrl": "http://exmple.com" +// } +// """) +// +// user must be(UserWithPicture("foo-123", Custom, "http://exmple.com")) +// } +// +// "write custom singleton values" in { +// val userJson = toJValue(UserWithPicture("foo-123", Custom, "http://exmple.com")) +// +// val Valid(expectedJson) = parseJSON(""" +// { +// "userId": "foo-123", +// "pictureSize": "bar", +// "pictureUrl": "http://exmple.com" +// } +// """) +// +// filter(userJson) must be(expectedJson) +// } +// +// "write and consequently read, which must produce the original value" in { +// val originalUser = UserWithPicture("foo-123", Medium, "http://exmple.com") +// val newUser = getFromJSON[UserWithPicture](compact(render(toJValue(originalUser)))) +// +// newUser must be(originalUser) +// } +// +// "read and write sealed trait with only one subtype" in { +// val json = +// """ +// { +// "userId": "foo-123", +// "pictureSize": "Medium", +// "pictureUrl": "http://example.com", +// "access": { +// "type": "Authorized", +// "project": "internal" +// } +// } +// """ +// val user = getFromJSON[UserWithPicture](json) +// +// user must be( +// UserWithPicture( +// "foo-123", +// Medium, +// "http://example.com", +// Some(Access.Authorized("internal")))) +// +// val newJson = toJValue[UserWithPicture](user) +// Valid(newJson) must be(parseJSON(json)) +// +// val Valid(newUser) = fromJValue[UserWithPicture](newJson) +// newUser must be(user) +// } +// } +// +// private def filter(jvalue: JValue): JValue = +// jvalue.removeField { +// case (_, JNothing) => true +// case _ => false +// } +//} +// +//sealed abstract class PictureSize(val weight: Int, val height: Int) +// +//case object Small extends PictureSize(100, 100) {} +//case object Medium extends PictureSize(500, 450) +//case object Big extends PictureSize(1024, 2048) +// +//@JSONTypeHint("bar") +//case object Custom extends PictureSize(1, 2) +// +////object PictureSize { +//// implicit val json: JSON[PictureSize] = deriveJSON[PictureSize] +////} +// +////sealed trait Access +////object Access { +//// // only one sub-type +//// case class Authorized(project: String) extends Access +//// +//// implicit val json: JSON[Access] = deriveJSON[Access] +////} +// +//case class UserWithPicture( +// userId: String, +// pictureSize: PictureSize, +// pictureUrl: String +// /*access: Option[Access] = None*/ ) +// +//object UserWithPicture { +// implicit val json: JSON[UserWithPicture] = deriveJSON[UserWithPicture] +//} diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/ForProductNSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/ForProductNSpec.scala new file mode 100644 index 00000000..c5f92b03 --- /dev/null +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/ForProductNSpec.scala @@ -0,0 +1,35 @@ +//package io.sphere.json +// +//import java.util.UUID +// +//import io.sphere.json.ToJSONProduct._ +//import org.json4s._ +//import org.scalatest.matchers.must.Matchers +//import org.scalatest.wordspec.AnyWordSpec +// +//case class User(id: UUID, firstName: String, age: Int) +// +//class ForProductNSpec extends AnyWordSpec with Matchers { +// +// "forProductN helper methods" must { +// "serialize" in { +// implicit val encodeUser: ToJSON[User] = forProduct3(u => +// ( +// "id" -> u.id, +// "first_name" -> u.firstName, +// "age" -> u.age +// )) +// +// val id = UUID.randomUUID() +// val json = toJValue(User(id, "bidule", 109)) +// json must be( +// JObject( +// List( +// "id" -> JString(id.toString), +// "first_name" -> JString("bidule"), +// "age" -> JLong(109) +// ))) +// } +// } +// +//} diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala new file mode 100644 index 00000000..3f5d5483 --- /dev/null +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala @@ -0,0 +1,131 @@ +package io.sphere.json + +import org.scalatest.OptionValues +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec +import io.sphere.json.generic._ + +object JSONEmbeddedSpec { + + case class Embedded(value1: String, value2: Int) + + object Embedded { + implicit val json: JSON[Embedded] = deriveJSON[Embedded] + } + + case class Test1(name: String, @JSONEmbedded embedded: Embedded) + + object Test1 { + implicit val json: JSON[Test1] = deriveJSON[Test1] + } + + case class Test2(name: String, @JSONEmbedded embedded: Option[Embedded] = None) + + object Test2 { + implicit val json: JSON[Test2] = deriveJSON + } + + case class SubTest4(@JSONEmbedded embedded: Embedded) + object SubTest4 { + implicit val json: JSON[SubTest4] = deriveJSON + } + + case class Test4(subField: Option[SubTest4] = None) + object Test4 { + implicit val json: JSON[Test4] = deriveJSON + } +} + +class JSONEmbeddedSpec extends AnyWordSpec with Matchers with OptionValues { + import JSONEmbeddedSpec._ + + "JSONEmbedded" should { + "flatten the json in one object" in { + val json = + """{ + | "name": "ze name", + | "value1": "ze value1", + | "value2": 45 + |} + """.stripMargin + val test1 = getFromJSON[Test1](json) + test1.name mustEqual "ze name" + test1.embedded.value1 mustEqual "ze value1" + test1.embedded.value2 mustEqual 45 + + val result = toJSON(test1) + parseJSON(result) mustEqual parseJSON(json) + } + + "validate that the json contains all needed fields" in { + val json = + """{ + | "name": "ze name", + | "value1": "ze value1" + |} + """.stripMargin + fromJSON[Test1](json).isInvalid must be(true) + fromJSON[Test1]("""{"name": "a"}""").isInvalid must be(true) + } + + "support optional embedded attribute" in { + val json = + """{ + | "name": "ze name", + | "value1": "ze value1", + | "value2": 45 + |} + """.stripMargin + val test2 = getFromJSON[Test2](json) + test2.name mustEqual "ze name" + test2.embedded.value.value1 mustEqual "ze value1" + test2.embedded.value.value2 mustEqual 45 + + val result = toJSON(test2) + parseJSON(result) mustEqual parseJSON(json) + } + + "ignore unknown fields" in { + val json = + """{ + | "name": "ze name", + | "value1": "ze value1", + | "value2": 45, + | "value3": true + |} + """.stripMargin + val test2 = getFromJSON[Test2](json) + test2.name mustEqual "ze name" + test2.embedded.value.value1 mustEqual "ze value1" + test2.embedded.value.value2 mustEqual 45 + } + + "check for sub-fields" in { + val json = + """ + { + "subField": { + "value1": "ze value1", + "value2": 45 + } + } + """ + val test4 = getFromJSON[Test4](json) + test4.subField.value.embedded.value1 mustEqual "ze value1" + test4.subField.value.embedded.value2 mustEqual 45 + } + + "support the absence of optional embedded attribute" ignore { + val json = """{ "name": "ze name" }""" + val test2 = getFromJSON[Test2](json) + test2.name mustEqual "ze name" + test2.embedded mustEqual None + } + + "validate the absence of some embedded attributes" in { + val json = """{ "name": "ze name", "value1": "ze value1" }""" + fromJSON[Test2](json).isInvalid must be(true) + } + } + +} diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala new file mode 100644 index 00000000..50d2b0a7 --- /dev/null +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala @@ -0,0 +1,395 @@ +package io.sphere.json + +import cats.data.Validated.{Invalid, Valid} +import cats.data.ValidatedNel +import cats.syntax.apply.* +import org.json4s.JsonAST.* +import io.sphere.json.field +import io.sphere.json.generic.* +import io.sphere.util.Money +import org.joda.time.* +import org.scalatest.matchers.must.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.json4s.DefaultJsonFormats.given + +object JSONSpec { + case class Project(nr: Int, name: String, version: Int = 1, milestones: List[Milestone] = Nil) + case class Milestone(name: String, date: Option[DateTime] = None) + + sealed abstract class Animal + case class Dog(name: String) extends Animal + case class Cat(name: String) extends Animal + case class Bird(name: String) extends Animal + + sealed trait GenericBase[A] + case class GenericA[A](a: A) extends GenericBase[A] + case class GenericB[A](a: A) extends GenericBase[A] + + object Singleton + + sealed abstract class SingletonEnum + case object SingletonA extends SingletonEnum + case object SingletonB extends SingletonEnum + case object SingletonC extends SingletonEnum + + sealed trait Mixed + case object SingletonMixed extends Mixed + case class RecordMixed(i: Int) extends Mixed + + object ScalaEnum extends Enumeration { + val One, Two, Three = Value + } + + case class Node( + value: Option[List[Node]]) // JSON instances for recursive data types cannot be derived +} + +class JSONSpec extends AnyFunSpec with Matchers { + import JSONSpec._ + + describe("JSON") { + it("must read/write a custom class using custom typeclass instances") { + import JSONSpec.{Milestone, Project} + + implicit object MilestoneJSON extends JSON[Milestone] { + def write(m: Milestone): JValue = JObject( + JField("name", JString(m.name)) :: + JField("date", toJValue(m.date)) :: Nil + ) + def read(j: JValue): ValidatedNel[JSONError, Milestone] = j match { + case o: JObject => + (field[String]("name")(o), field[Option[DateTime]]("date")(o)).mapN(Milestone) + case _ => fail("JSON object expected.") + } + } + implicit object ProjectJSON extends JSON[Project] { + def write(p: Project): JValue = JObject( + JField("nr", JInt(p.nr)) :: + JField("name", JString(p.name)) :: + JField("version", JInt(p.version)) :: + JField("milestones", toJValue(p.milestones)) :: + Nil + ) + def read(jval: JValue): ValidatedNel[JSONError, Project] = jval match { + case o: JObject => + ( + field[Int]("nr")(o), + field[String]("name")(o), + field[Int]("version", Some(1))(o), + field[List[Milestone]]("milestones", Some(Nil))(o)).mapN(Project.apply) + case _ => fail("JSON object expected.") + } + } + + val proj = Project(42, "Linux") + fromJSON[Project](toJSON(proj)) must equal(Valid(proj)) + + // Now some invalid JSON to test the error accumulation + val wrongTypeJSON = """ + { + "nr":"1", + "name":23, + "version":1, + "milestones":[{"name":"Bravo", "date": "xxx"}] + } + """ + + val Invalid(errs) = fromJSON[Project](wrongTypeJSON) + errs.toList must equal( + List( + JSONFieldError(List("nr"), "JSON Number in the range of an Int expected."), + JSONFieldError(List("name"), "JSON String expected."), + JSONFieldError(List("milestones", "date"), "Failed to parse date/time: xxx") + )) + + // Now without a version value and without a milestones list. Defaults should apply. + val noVersionJSON = """{"nr":1,"name":"Linux"}""" + fromJSON[Project](noVersionJSON) must equal(Valid(Project(1, "Linux"))) + } + + it("must fail reading wrong currency code.") { + val wrongMoney = """{"currencyCode":"WRONG","centAmount":1000}""" + fromJSON[Money](wrongMoney).isInvalid must be(true) + } + + it("must provide derived JSON instances for product types (case classes)") { + import JSONSpec.{Milestone, Project} + implicit val milestoneJSON: JSON[Milestone] = deriveJSON[Milestone] + implicit val projectJSON: JSON[Project] = deriveJSON[Project] + val proj = + Project(42, "Linux", 7, Milestone("1.0") :: Milestone("2.0") :: Milestone("3.0") :: Nil) + fromJSON[Project](toJSON(proj)) must equal(Valid(proj)) + } + + it("must handle empty String") { + val Invalid(err) = fromJSON[Int]("") + err.toList.head mustBe a[JSONParseError] + } + + it("must provide user-friendly error by empty String") { + val Invalid(err) = fromJSON[Int]("") + err.toList mustEqual List(JSONParseError("No content to map due to end-of-input")) + } + + it("must handle incorrect json") { + val Invalid(err) = fromJSON[Int]("""{"key: "value"}""") + err.toList.head mustBe a[JSONParseError] + } + + it("must provide user-friendly error by incorrect json") { + val Invalid(err) = fromJSON[Int]("""{"key: "value"}""") + err.toList mustEqual List(JSONParseError( + "Unexpected character ('v' (code 118)): was expecting a colon to separate field name and value")) + } + +// it("must provide derived JSON instances for sum types") { +// import io.sphere.json.generic.JSON.derived +// implicit val animalJSON = deriveJSON[Animal] +// List(Bird("Peewee"), Dog("Hasso"), Cat("Felidae")).foreach { +// a: Animal => +// fromJSON[Animal](toJSON(a)) must equal(Valid(a)) +// } +// } + + it("must provide derived instances for product types with concrete type parameters") { + import io.sphere.json.generic.JSON.derived + implicit val aJSON: JSON[GenericA[String]] = deriveJSON[GenericA[String]] + val a = GenericA("hello") + fromJSON[GenericA[String]](toJSON(a)) must equal(Valid(a)) + } + + it("must provide derived instances for product types with generic type parameters") { + import io.sphere.json.generic.JSON.derived + implicit def aJSON[A: JSON]: JSON[GenericA[A]] = deriveJSON[GenericA[A]] + val a = GenericA("hello") + fromJSON[GenericA[String]](toJSON(a)) must equal(Valid(a)) + } + +// it("must provide derived instances for singleton objects") { +// import io.sphere.json.generic.JSON.derived +// implicit val singletonJSON: JSON[JSONSpec.Singleton.type] = deriveJSON[JSONSpec.Singleton.type] +// +// val json = s"""[${toJSON(Singleton)}]""" +// withClue(json) { +// fromJSON[Seq[Singleton.type]](json) must equal(Valid(Seq(Singleton))) +// } +// +// implicit val singleEnumJSON: JSON[SingletonEnum] = deriveJSON[SingletonEnum] +// List(SingletonA, SingletonB, SingletonC).foreach { s => +// fromJSON[SingletonEnum](toJSON(s)) must equal(Valid(s)) +// } +// } +// +// it("must provide derived instances for sum types with a mix of case class / object") { +// import io.sphere.json.generic.JSON.derived +// implicit val mixedJSON: JSON[Mixed] = deriveJSON[Mixed] +// List(SingletonMixed, RecordMixed(1)).foreach { +// m: Mixed => +// fromJSON[Mixed](toJSON(m)) must equal(Valid(m)) +// } +// } +// +// it("must provide derived instances for scala.Enumeration") { +// import io.sphere.json.generic.JSON.derived +// implicit val scalaEnumJSON: JSON[JSONSpec.ScalaEnum.Value] = deriveJSON[ScalaEnum.Value] +// ScalaEnum.values.foreach { v => +// val json = s"""[${toJSON(v)}]""" +// withClue(json) { +// fromJSON[Seq[ScalaEnum.Value]](json) must equal(Valid(Seq(v))) +// } +// } +// } +// +// it("must handle subclasses correctly in `jsonTypeSwitch`") { +// implicit val jsonImpl = TestSubjectBase.json +// +// val testSubjects = List[TestSubjectBase]( +// TestSubjectConcrete1("testSubject1"), +// TestSubjectConcrete2("testSubject2"), +// TestSubjectConcrete3("testSubject3"), +// TestSubjectConcrete4("testSubject4") +// ) +// +// testSubjects.foreach { testSubject => +// val json = toJSON(testSubject) +// withClue(json) { +// fromJSON[TestSubjectBase](json) must equal(Valid(testSubject)) +// } +// } +// +// } +// +// } +// +// describe("ToJSON and FromJSON") { +// it("must provide derived JSON instances for sum types") { +// // ToJSON +// implicit val birdToJSON = deriveJSON[Bird].write(Bird.apply _) +// implicit val dogToJSON = deriveJSON[Dog].write(Dog.apply _) +// implicit val catToJSON = toJsonProduct(Cat.apply _) +// implicit val animalToJSON = toJsonTypeSwitch[Animal, Bird, Dog, Cat](Nil) +// // FromJSON +// implicit val birdFromJSON = fromJsonProduct(Bird.apply _) +// implicit val dogFromJSON = fromJsonProduct(Dog.apply _) +// implicit val catFromJSON = fromJsonProduct(Cat.apply _) +// implicit val animalFromJSON = fromJsonTypeSwitch[Animal, Bird, Dog, Cat](Nil) +// +// List(Bird("Peewee"), Dog("Hasso"), Cat("Felidae")).foreach { +// a: Animal => +// fromJSON[Animal](toJSON(a)) must equal(Valid(a)) +// } +// } +// +// it("must provide derived instances for product types with concrete type parameters") { +// implicit val aToJSON = toJsonProduct(GenericA.apply[String] _) +// implicit val aFromJSON = fromJsonProduct(GenericA.apply[String] _) +// val a = GenericA("hello") +// fromJSON[GenericA[String]](toJSON(a)) must equal(Valid(a)) +// } +// +// it("must provide derived instances for singleton objects") { +// implicit val toSingletonJSON = toJsonSingleton(Singleton) +// implicit val fromSingletonJSON = fromJsonSingleton(Singleton) +// val json = s"""[${toJSON(Singleton)}]""" +// withClue(json) { +// fromJSON[Seq[Singleton.type]](json) must equal(Valid(Seq(Singleton))) +// } +// +// // ToJSON +// implicit val toSingleAJSON = toJsonSingleton(SingletonA) +// implicit val toSingleBJSON = toJsonSingleton(SingletonB) +// implicit val toSingleCJSON = toJsonSingleton(SingletonC) +// implicit val toSingleEnumJSON = +// toJsonSingletonEnumSwitch[SingletonEnum, SingletonA.type, SingletonB.type, SingletonC.type]( +// Nil) +// // FromJSON +// implicit val fromSingleAJSON = fromJsonSingleton(SingletonA) +// implicit val fromSingleBJSON = fromJsonSingleton(SingletonB) +// implicit val fromSingleCJSON = fromJsonSingleton(SingletonC) +// implicit val fromSingleEnumJSON = fromJsonSingletonEnumSwitch[ +// SingletonEnum, +// SingletonA.type, +// SingletonB.type, +// SingletonC.type](Nil) +// +// List(SingletonA, SingletonB, SingletonC).foreach { +// s: SingletonEnum => +// fromJSON[SingletonEnum](toJSON(s)) must equal(Valid(s)) +// } +// } +// +// it("must provide derived instances for sum types with a mix of case class / object") { +// // ToJSON +// implicit val toSingleJSON = toJsonProduct0(SingletonMixed) +// implicit val toRecordJSON = toJsonProduct(RecordMixed.apply _) +// implicit val toMixedJSON = toJsonTypeSwitch[Mixed, SingletonMixed.type, RecordMixed](Nil) +// // FromJSON +// implicit val fromSingleJSON = fromJsonProduct0(SingletonMixed) +// implicit val fromRecordJSON = fromJsonProduct(RecordMixed.apply _) +// implicit val fromMixedJSON = fromJsonTypeSwitch[Mixed, SingletonMixed.type, RecordMixed](Nil) +// List(SingletonMixed, RecordMixed(1)).foreach { +// m: Mixed => +// fromJSON[Mixed](toJSON(m)) must equal(Valid(m)) +// } +// } +// +// it("must provide derived instances for scala.Enumeration") { +// implicit val toScalaEnumJSON = toJsonEnum(ScalaEnum) +// implicit val fromScalaEnumJSON = fromJsonEnum(ScalaEnum) +// ScalaEnum.values.foreach { v => +// val json = s"""[${toJSON(v)}]""" +// withClue(json) { +// fromJSON[Seq[ScalaEnum.Value]](json) must equal(Valid(Seq(v))) +// } +// } +// } +// +// it("must handle subclasses correctly in `jsonTypeSwitch`") { +// // ToJSON +// implicit val to1 = toJsonProduct(TestSubjectConcrete1.apply _) +// implicit val to2 = toJsonProduct(TestSubjectConcrete2.apply _) +// implicit val to3 = toJsonProduct(TestSubjectConcrete3.apply _) +// implicit val to4 = toJsonProduct(TestSubjectConcrete4.apply _) +// implicit val toA = +// toJsonTypeSwitch[TestSubjectCategoryA, TestSubjectConcrete1, TestSubjectConcrete2](Nil) +// implicit val toB = +// toJsonTypeSwitch[TestSubjectCategoryB, TestSubjectConcrete3, TestSubjectConcrete4](Nil) +// implicit val toBase = +// toJsonTypeSwitch[TestSubjectBase, TestSubjectCategoryA, TestSubjectCategoryB](Nil) +// +// // FromJSON +// implicit val from1 = fromJsonProduct(TestSubjectConcrete1.apply _) +// implicit val from2 = fromJsonProduct(TestSubjectConcrete2.apply _) +// implicit val from3 = fromJsonProduct(TestSubjectConcrete3.apply _) +// implicit val from4 = fromJsonProduct(TestSubjectConcrete4.apply _) +// implicit val fromA = +// fromJsonTypeSwitch[TestSubjectCategoryA, TestSubjectConcrete1, TestSubjectConcrete2](Nil) +// implicit val fromB = +// fromJsonTypeSwitch[TestSubjectCategoryB, TestSubjectConcrete3, TestSubjectConcrete4](Nil) +// implicit val fromBase = +// fromJsonTypeSwitch[TestSubjectBase, TestSubjectCategoryA, TestSubjectCategoryB](Nil) +// +// val testSubjects = List[TestSubjectBase]( +// TestSubjectConcrete1("testSubject1"), +// TestSubjectConcrete2("testSubject2"), +// TestSubjectConcrete3("testSubject3"), +// TestSubjectConcrete4("testSubject4") +// ) +// +// testSubjects.foreach { testSubject => +// val json = toJSON(testSubject) +// withClue(json) { +// fromJSON[TestSubjectBase](json) must equal(Valid(testSubject)) +// } +// } +// +// } +// +// it("must provide derived JSON instances for product types (case classes)") { +// import JSONSpec.{Milestone, Project} +// // ToJSON +// implicit val milestoneToJSON = toJsonProduct(Milestone.apply _) +// implicit val projectToJSON = toJsonProduct(Project.apply _) +// // FromJSON +// implicit val milestoneFromJSON = fromJsonProduct(Milestone.apply _) +// implicit val projectFromJSON = fromJsonProduct(Project.apply _) +// +// val proj = +// Project(42, "Linux", 7, Milestone("1.0") :: Milestone("2.0") :: Milestone("3.0") :: Nil) +// fromJSON[Project](toJSON(proj)) must equal(Valid(proj)) +// } + } +} + +abstract class TestSubjectBase + +sealed abstract class TestSubjectCategoryA extends TestSubjectBase +sealed abstract class TestSubjectCategoryB extends TestSubjectBase + +@JSONTypeHint("foo") +case class TestSubjectConcrete1(c1: String) extends TestSubjectCategoryA +case class TestSubjectConcrete2(c2: String) extends TestSubjectCategoryA + +case class TestSubjectConcrete3(c3: String) extends TestSubjectCategoryB +case class TestSubjectConcrete4(c4: String) extends TestSubjectCategoryB + +object TestSubjectCategoryA { + + import io.sphere.json.generic.JSON.derived + val json: JSON[TestSubjectCategoryA] = deriveJSON[TestSubjectCategoryA] +} + +object TestSubjectCategoryB { + + import io.sphere.json.generic.JSON.derived + val json: JSON[TestSubjectCategoryB] = deriveJSON[TestSubjectCategoryB] +} + +//object TestSubjectBase { +// val json: JSON[TestSubjectBase] = { +// implicit val jsonA = TestSubjectCategoryA.json +// implicit val jsonB = TestSubjectCategoryB.json +// +// jsonTypeSwitch[TestSubjectBase, TestSubjectCategoryA, TestSubjectCategoryB](Nil) +// } +//} diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/NullHandlingSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/NullHandlingSpec.scala new file mode 100644 index 00000000..fe0faec4 --- /dev/null +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/NullHandlingSpec.scala @@ -0,0 +1,68 @@ +package io.sphere.json + +import io.sphere.json.generic._ +import org.json4s.JsonAST.{JNothing, JObject} +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class NullHandlingSpec extends AnyWordSpec with Matchers { + "JSON deserialization" must { + "accept undefined fields and use default values for them" ignore { + val jeans = getFromJSON[Jeans]("{}") + + jeans must be(Jeans(None, None, Set.empty, "secret")) + } + + "accept null values and use default values for them" ignore { + val jeans = getFromJSON[Jeans](""" + { + "leftPocket": null, + "rightPocket": null, + "backPocket": null, + "hiddenPocket": null + } + """) + + jeans must be(Jeans(None, None, Set.empty, "secret")) + } + + "accept JNothing values and use default values for them" ignore { + val jeans = getFromJValue[Jeans]( + JObject( + "leftPocket" -> JNothing, + "rightPocket" -> JNothing, + "backPocket" -> JNothing, + "hiddenPocket" -> JNothing)) + + jeans must be(Jeans(None, None, Set.empty, "secret")) + } + + "accept not-null values and use them" in { + val jeans = getFromJSON[Jeans](""" + { + "leftPocket": "Axe", + "rightPocket": "Magic powder", + "backPocket": ["Magic wand", "Rusty sword"], + "hiddenPocket": "The potion of healing" + } + """) + + jeans must be( + Jeans( + Some("Axe"), + Some("Magic powder"), + Set("Magic wand", "Rusty sword"), + "The potion of healing")) + } + } +} + +case class Jeans( + leftPocket: Option[String] = None, + rightPocket: Option[String], + backPocket: Set[String] = Set.empty, + hiddenPocket: String = "secret") + +object Jeans { + implicit val json: JSON[Jeans] = deriveJSON[Jeans] +} diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/OptionReaderSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/OptionReaderSpec.scala new file mode 100644 index 00000000..f72e0378 --- /dev/null +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/OptionReaderSpec.scala @@ -0,0 +1,150 @@ +package io.sphere.json + +import io.sphere.json.generic._ +import org.json4s.{JArray, JLong, JNothing, JObject, JString} +import org.scalatest.OptionValues +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec +import org.json4s.DefaultJsonFormats.given + +object OptionReaderSpec { + + case class SimpleClass(value1: String, value2: Int) + + object SimpleClass { + implicit val json: JSON[SimpleClass] = deriveJSON[SimpleClass] + } + + case class ComplexClass(name: String, simpleClass: Option[SimpleClass]) + + object ComplexClass { + implicit val json: JSON[ComplexClass] = deriveJSON[ComplexClass] + } + + case class MapClass(id: Long, map: Option[Map[String, String]]) + object MapClass { + implicit val json: JSON[MapClass] = deriveJSON[MapClass] + } + + case class ListClass(id: Long, list: Option[List[String]]) + object ListClass { + implicit val json: JSON[ListClass] = deriveJSON[ListClass] + } +} + +class OptionReaderSpec extends AnyWordSpec with Matchers with OptionValues { + import OptionReaderSpec._ + + "OptionReader" should { + "handle presence of all fields" in { + val json = + """{ + | "value1": "a", + | "value2": 45 + |} + """.stripMargin + val result = getFromJSON[Option[SimpleClass]](json) + result.value.value1 mustEqual "a" + result.value.value2 mustEqual 45 + } + + "handle presence of all fields mixed with ignored fields" in { + val json = + """{ + | "value1": "a", + | "value2": 45, + | "value3": "b" + |} + """.stripMargin + val result = getFromJSON[Option[SimpleClass]](json) + result.value.value1 mustEqual "a" + result.value.value2 mustEqual 45 + } + + "handle presence of not all the fields" in { + val json = """{ "value1": "a" }""" + fromJSON[Option[SimpleClass]](json).isInvalid must be(true) + } + + "handle absence of all fields" in { + val json = "{}" + val result = getFromJSON[Option[SimpleClass]](json) + result must be(None) + } + + "handle optional map" ignore { + getFromJValue[MapClass](JObject("id" -> JLong(1L))) mustEqual MapClass(1L, None) + + getFromJValue[MapClass](JObject("id" -> JLong(1L), "map" -> JObject())) mustEqual + MapClass(1L, Some(Map.empty)) + + getFromJValue[MapClass]( + JObject("id" -> JLong(1L), "map" -> JObject("a" -> JString("b")))) mustEqual + MapClass(1L, Some(Map("a" -> "b"))) + + toJValue[MapClass](MapClass(1L, None)) mustEqual + JObject("id" -> JLong(1L), "map" -> JNothing) + toJValue[MapClass](MapClass(1L, Some(Map()))) mustEqual + JObject("id" -> JLong(1L), "map" -> JObject()) + toJValue[MapClass](MapClass(1L, Some(Map("a" -> "b")))) mustEqual + JObject("id" -> JLong(1L), "map" -> JObject("a" -> JString("b"))) + } + + "handle optional list" ignore { + getFromJValue[ListClass]( + JObject("id" -> JLong(1L), "list" -> JArray(List(JString("hi"))))) mustEqual + ListClass(1L, Some(List("hi"))) + getFromJValue[ListClass](JObject("id" -> JLong(1L), "list" -> JArray(List.empty))) mustEqual + ListClass(1L, Some(List())) + getFromJValue[ListClass](JObject("id" -> JLong(1L))) mustEqual + ListClass(1L, None) + + toJValue(ListClass(1L, Some(List("hi")))) mustEqual JObject( + "id" -> JLong(1L), + "list" -> JArray(List(JString("hi")))) + toJValue(ListClass(1L, Some(List.empty))) mustEqual JObject( + "id" -> JLong(1L), + "list" -> JArray(List.empty)) + toJValue(ListClass(1L, None)) mustEqual JObject("id" -> JLong(1L), "list" -> JNothing) + } + + "handle absence of all fields mixed with ignored fields" ignore { + val json = """{ "value3": "a" }""" + val result = getFromJSON[Option[SimpleClass]](json) + result must be(None) + } + + "consider all fields if the data type does not impose any restriction" in { + val json = + """{ + | "key1": "value1", + | "key2": "value2" + |} + """.stripMargin + val expected = Map("key1" -> "value1", "key2" -> "value2") + val result = getFromJSON[Map[String, String]](json) + result mustEqual expected + + val maybeResult = getFromJSON[Option[Map[String, String]]](json) + maybeResult.value mustEqual expected + } + + "parse optional element" in { + val json = + """{ + | "name": "ze name", + | "simpleClass": { + | "value1": "value1", + | "value2": 42 + | } + |} + """.stripMargin + val result = getFromJSON[ComplexClass](json) + result.simpleClass.value.value1 mustEqual "value1" + result.simpleClass.value.value2 mustEqual 42 + + parseJSON(toJSON(result)) mustEqual parseJSON(json) + } + } + +} diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/TypesSwitchSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/TypesSwitchSpec.scala new file mode 100644 index 00000000..88f49334 --- /dev/null +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/TypesSwitchSpec.scala @@ -0,0 +1,87 @@ +//package io.sphere.json +// +//import io.sphere.json.generic.{TypeSelectorContainer, deriveJSON, jsonTypeSwitch} +//import org.json4s._ +//import org.scalatest.matchers.must.Matchers +//import org.scalatest.wordspec.AnyWordSpec +// +//class TypesSwitchSpec extends AnyWordSpec with Matchers { +// import TypesSwitchSpec._ +// +// "jsonTypeSwitch" must { +// "combine different sum types tree" in { +// val m: Seq[Message] = List( +// TypeA.ClassA1(23), +// TypeA.ClassA2("world"), +// TypeB.ClassB1(valid = false), +// TypeB.ClassB2(Seq("a23", "c62"))) +// +// val jsons = m.map(Message.json.write) +// jsons must be( +// List( +// JObject("number" -> JLong(23), "type" -> JString("ClassA1")), +// JObject("name" -> JString("world"), "type" -> JString("ClassA2")), +// JObject("valid" -> JBool(false), "type" -> JString("ClassB1")), +// JObject( +// "references" -> JArray(List(JString("a23"), JString("c62"))), +// "type" -> JString("ClassB2")) +// )) +// +// val messages = jsons.map(Message.json.read).map(_.toOption.get) +// messages must be(m) +// } +// } +// +// "TypeSelectorContainer" must { +// "have information about type value discriminators" in { +// val selectors = Message.json.typeSelectors +// selectors.map(_.typeValue) must contain.allOf( +// "ClassA1", +// "ClassA2", +// "TypeA", +// "ClassB1", +// "ClassB2", +// "TypeB") +// +// // I don't think it's useful to allow different type fields. How is it possible to deserialize one json +// // if different type fields are used? +// selectors.map(_.typeField) must be(List("type", "type", "type", "type", "type", "type")) +// +// selectors.map(_.clazz.getName) must contain.allOf( +// "io.sphere.json.TypesSwitchSpec$TypeA$ClassA1", +// "io.sphere.json.TypesSwitchSpec$TypeA$ClassA2", +// "io.sphere.json.TypesSwitchSpec$TypeA", +// "io.sphere.json.TypesSwitchSpec$TypeB$ClassB1", +// "io.sphere.json.TypesSwitchSpec$TypeB$ClassB2", +// "io.sphere.json.TypesSwitchSpec$TypeB" +// ) +// } +// } +// +//} +// +//object TypesSwitchSpec { +// +// trait Message +// object Message { +// // this can be dangerous is the same class name is used in both sum types +// // ex if we define TypeA.Class1 && TypeB.Class1 +// // as both will use the same type value discriminator +// implicit val json: JSON[Message] with TypeSelectorContainer = +// jsonTypeSwitch[Message, TypeA, TypeB](Nil) +// } +// +// sealed trait TypeA extends Message +// object TypeA { +// case class ClassA1(number: Int) extends TypeA +// case class ClassA2(name: String) extends TypeA +// implicit val json: JSON[TypeA] = deriveJSON[TypeA] +// } +// +// sealed trait TypeB extends Message +// object TypeB { +// case class ClassB1(valid: Boolean) extends TypeB +// case class ClassB2(references: Seq[String]) extends TypeB +// implicit val json: JSON[TypeB] = deriveJSON[TypeB] +// } +//} diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala new file mode 100644 index 00000000..caa270e1 --- /dev/null +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala @@ -0,0 +1,44 @@ +package io.sphere.json.generic + +import io.sphere.json._ +import io.sphere.json.generic._ +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class DefaultValuesSpec extends AnyWordSpec with Matchers { + import DefaultValuesSpec._ + + "deriving JSON" must { + "handle default values" in { + val json = "{ }" + val test = getFromJSON[Test](json) + test.value1 must be("hello") + test.value2 must be(None) + test.value3 must be(Some("hi")) + } + "handle Option with no explicit default values" ignore { + val json = "{ }" + val test2 = getFromJSON[Test2](json) + test2.value1 must be("hello") + test2.value2 must be(None) + } + } +} + +object DefaultValuesSpec { + case class Test( + value1: String = "hello", + value2: Option[String] = None, + value3: Option[String] = Some("hi") + ) + object Test { + implicit val json: JSON[Test] = deriveJSON[Test] + } + case class Test2( + value1: String = "hello", + value2: Option[String] + ) + object Test2 { + implicit val json: JSON[Test2] = deriveJSON[Test2] + } +} diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JSONKeySpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JSONKeySpec.scala new file mode 100644 index 00000000..459755a8 --- /dev/null +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JSONKeySpec.scala @@ -0,0 +1,47 @@ +package io.sphere.json.generic + +import org.json4s.MonadicJValue.jvalueToMonadic +import org.json4s.jvalue2readerSyntax +import io.sphere.json._ +import io.sphere.json.generic._ +import org.json4s.DefaultReaders._ +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class JSONKeySpec extends AnyWordSpec with Matchers { + import JSONKeySpec._ + + "deriving JSON" must { + "rename fields annotated with @JSONKey" in { + val test = + Test(value1 = "value1", value2 = "value2", subTest = SubTest(value2 = "other_value2")) + + val json = toJValue(test) + (json \ "value1").as[Option[String]] must be(Some("value1")) + (json \ "value2").as[Option[String]] must be(None) + (json \ "new_value_2").as[Option[String]] must be(Some("value2")) + (json \ "new_sub_value_2").as[Option[String]] must be(Some("other_value2")) + + val newTest = getFromJValue[Test](json) + newTest must be(test) + } + } +} + +object JSONKeySpec { + case class SubTest( + @JSONKey("new_sub_value_2") value2: String + ) + object SubTest { + implicit val json: JSON[SubTest] = deriveJSON[SubTest] + } + + case class Test( + value1: String, + @JSONKey("new_value_2") value2: String, + @JSONEmbedded subTest: SubTest + ) + object Test { + implicit val json: JSON[Test] = deriveJSON[Test] + } +} diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala new file mode 100644 index 00000000..32cc5428 --- /dev/null +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala @@ -0,0 +1,69 @@ +package io.sphere.json.generic + +import cats.data.Validated.Valid +import io.sphere.json._ +import org.json4s._ +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class JsonTypeHintFieldSpec extends AnyWordSpec with Matchers { + import JsonTypeHintFieldSpec._ + + "JSONTypeHintField" must { + "allow to set another field to distinguish between types (toJValue)" ignore { + val user = UserWithPicture("foo-123", Medium, "http://example.com") + val expected = JObject( + List( + "userId" -> JString("foo-123"), + "pictureSize" -> JObject(List("pictureType" -> JString("Medium"))), + "pictureUrl" -> JString("http://example.com"))) + val _impl = implicitly[JSON[UserWithPicture]] + println(_impl) + + val json = toJValue[UserWithPicture](user) + json must be(expected) + } + + "allow to set another field to distinguish between types (fromJSON)" in { + val json = + """ + { + "userId": "foo-123", + "pictureSize": { "pictureType": "Medium" }, + "pictureUrl": "http://example.com" + } + """ + + val Valid(user) = fromJSON[UserWithPicture](json) + + user must be(UserWithPicture("foo-123", Medium, "http://example.com")) + } + } + +} + +object JsonTypeHintFieldSpec { + + @JSONTypeHintField(value = "pictureType") + sealed trait PictureSize + @JSONTypeHintField(value = "pictureType") + case object Small extends PictureSize + @JSONTypeHintField(value = "pictureType") + case object Medium extends PictureSize + @JSONTypeHintField(value = "pictureType") + case object Big extends PictureSize + + object PictureSize { + import io.sphere.json.generic.JSON.given + import io.sphere.json.generic.deriveJSON + implicit val json: JSON[PictureSize] = deriveJSON[PictureSize] + } + + case class UserWithPicture(userId: String, pictureSize: PictureSize, pictureUrl: String) + + object UserWithPicture { + import io.sphere.json.generic.JSON.given + import io.sphere.json.generic.deriveJSON + implicit val json: JSON[UserWithPicture] = deriveJSON[UserWithPicture] + } +} From 8f2d38ebfaa37939e75a386a0724782c92d387f9 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 24 May 2024 16:59:58 +0200 Subject: [PATCH 029/142] Fix tests --- .../mongo/generic/AnnotationReader.scala | 13 +++- .../io/sphere/mongo/generic/Derivation.scala | 25 +++--- .../io/sphere/mongo/generic/generic.scala | 78 ------------------- .../io/sphere/mongo/SerializationTest.scala | 15 ++-- 4 files changed, 31 insertions(+), 100 deletions(-) delete mode 100644 mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala index 04d273dc..ccca706b 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala @@ -5,12 +5,12 @@ import scala.quoted.{Expr, Quotes, Type, Varargs} private type MA = MongoAnnotation case class Field( - name: String, + rawName: String, embedded: Boolean, ignored: Boolean, mongoKey: Option[MongoKey], defaultArgument: Option[Any]) { - val fieldName: String = mongoKey.map(_.value).getOrElse(name) + val name: String = mongoKey.map(_.value).getOrElse(rawName) } case class CaseClassMetaData( name: String, @@ -30,6 +30,13 @@ case class TraitMetaData( } object AnnotationReader { + + def mongoEnum(e: Enumeration): TypedMongoFormat[e.Value] = new TypedMongoFormat[e.Value] { + def toMongoValue(a: e.Value): MongoType = a.toString + + def fromMongoValue(any: MongoType): e.Value = e.withName(any.asInstanceOf[String]) + } + inline def readTraitMetaData[T]: TraitMetaData = ${ readTraitMetaDataImpl[T] } inline def readCaseClassMetaData[T]: CaseClassMetaData = ${ readCaseClassMetaDataImpl[T] } @@ -108,7 +115,7 @@ class AnnotationReader(using q: Quotes): '{ Field( - name = $name, + rawName = $name, embedded = $embedded, ignored = $ignored, mongoKey = $mongoKey, diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala index fb08450a..fb19b9b2 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala @@ -40,10 +40,10 @@ object TypedMongoFormat: private def addField(bson: BasicDBObject, field: Field, mongoType: MongoType) = mongoType match - case s: SimpleMongoType => bson.put(field.fieldName, s) + case s: SimpleMongoType => bson.put(field.name, s) case innerBson: BasicDBObject => if (field.embedded) innerBson.entrySet().forEach(p => bson.put(p.getKey, p.getValue)) - else bson.put(field.fieldName, innerBson) + else bson.put(field.name, innerBson) case MongoNothing => private object Derivation: @@ -94,8 +94,8 @@ object TypedMongoFormat: private val fieldsAndFormatters = caseClassMetaData.fields.zip(formatters) override val fieldNames: Vector[String] = fieldsAndFormatters.flatMap((field, formatter) => - if (field.embedded) formatter.fieldNames :+ field.name - else Vector(field.name)) + if (field.embedded) formatter.fieldNames :+ field.rawName + else Vector(field.rawName)) override def toMongoValue(a: A): MongoType = val bson = new BasicDBObject() @@ -110,20 +110,21 @@ object TypedMongoFormat: mongoType match case bson: BasicDBObject => val fields = fieldsAndFormatters - .map { case (Field(name, embedded, ignored, mongoKey, defaultArgument), format) => - def defaultValue = defaultArgument.orElse(format.default) - if (ignored) - default.getOrElse { + .map { case (field, format) => + def defaultValue = field.defaultArgument.orElse(format.default) + if (field.ignored) + defaultValue.getOrElse { throw new Exception( - s"Missing default parameter value for ignored field `$name` on deserialization.") + s"Missing default parameter value for ignored field `${field.name}` on deserialization.") } - else if (embedded) format.fromMongoValue(bson) + else if (field.embedded) format.fromMongoValue(bson) else { - val value = bson.get(name) + val value = bson.get(field.name) if (value ne null) format.fromMongoValue(value.asInstanceOf[MongoType]) else defaultValue.getOrElse { - throw new Exception(s"Missing required field '$name' on deserialization.") + throw new Exception( + s"Missing required field '${field.name}' on deserialization.") } } } diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala deleted file mode 100644 index 2519a6ee..00000000 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/generic.scala +++ /dev/null @@ -1,78 +0,0 @@ -package io.sphere.mongo.generic - -import io.sphere.mongo.format.MongoFormat - -import scala.quoted.* - -//inline def deriveMongoFormat[A]: MongoFormat[A] = ${ deriveMongoFormatImpl } - -def deriveMongoFormatImpl[A](using tpe: Type[A], q: Quotes): Expr[MongoFormat[A]] = - import q.reflect.* - val typeRepr = TypeRepr.of[A] - val symbol = TypeTree.of[A].symbol - - if typeRepr <:< TypeRepr.of[Enumeration#Value] then - val enumTerm = typeRepr match - case TypeRef(tr: TermRef, _) => tr - case _ => report.errorAndAbort("no Enumeration found") - Apply( - Ref(Symbol.requiredMethod("io.sphere.mongo.generic.mongoEnum")), - Ident(enumTerm) :: Nil - ).asExprOf[MongoFormat[A]] - - else if symbol.flags.is(Flags.Case) && symbol.flags.is(Flags.Module) then - // not sure if this case is ever used, at least it's not tested in this library - val moduleName = typeRepr match { - case TermRef(_, module) => module - case _ => report.errorAndAbort("type does not refer to a stable value") - } - Apply( - TypeApply( - Ref(Symbol.requiredMethod("io.sphere.mongo.generic.mongoProduct0")), - TypeIdent(typeRepr.typeSymbol) :: Nil - ), - Ident(TermRef(typeRepr, moduleName)) :: Nil - ).asExprOf[MongoFormat[A]] - - else if (symbol.flags.is(Flags.Case)) - println(s"Hello Case! $symbol") - '{ dummyFormat[A] } - - else if (symbol.flags.is(Flags.Module)) - // has been unused with an implementation that called the non-existing function "mongoSingleton" - report.errorAndAbort("MongoFormat for stand-alone modules is not supported.") - - else - println(".......") - '{ dummyFormat[A] } - -end deriveMongoFormatImpl - -def dummyFormat[A]: MongoFormat[A] = new MongoFormat[A]: - override def toMongoValue(a: A): Any = ??? - override def fromMongoValue(any: Any): A = ??? - -def mongoEnum(e: Enumeration): MongoFormat[e.Value] = new MongoFormat[e.Value] { - def toMongoValue(a: e.Value): Any = a.toString - def fromMongoValue(any: Any): e.Value = e.withName(any.asInstanceOf[String]) -} - -def mongoProduct0[T <: Product](singleton: T): MongoFormat[T] = ??? /*{ - val (typeField, typeValue) = mongoProduct0Type(singleton) - new MongoFormat[T] { - override def toMongoValue(a: T): Any = { - val dbo = new BasicDBObject() - dbo.append(typeField, typeValue) - dbo - } - - override def fromMongoValue(any: Any): T = any match { - case o: BSONObject => findTypeValue(o, typeField) match { - case Some(t) if t == typeValue => singleton - case Some(t) => sys.error("Invalid type value '" + t + "'. Excepted '%s'".format(typeValue)) - case None => sys.error("Missing type field.") - } - case _ => sys.error("DB object excepted.") - } - } -}*/ diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala index 82149f1e..c4895897 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala @@ -1,10 +1,11 @@ package io.sphere.mongo import com.mongodb.BasicDBObject -import io.sphere.mongo.generic.{DefaultMongoFormats, TypedMongoFormat} +import io.sphere.mongo.generic.{AnnotationReader, DefaultMongoFormats, TypedMongoFormat} import io.sphere.mongo.generic.DefaultMongoFormats.given import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec + import java.util.UUID object ProductTypes: @@ -168,13 +169,13 @@ class SerializationTest extends AnyWordSpec with Matchers: } "serialize and deserialize enumerations" in { - // val mongo: TypedMongoFormat[Color.Value] = io.sphere.mongo.generic.deriveMongoFormat + val mongo: TypedMongoFormat[Color.Value] = AnnotationReader.mongoEnum(Color) - // // mongo java driver knows how to encode/decode Strings - // val serializedObject = mongo.toMongoValue(Color.Red).asInstanceOf[String] - // serializedObject must be("Red") + // mongo java driver knows how to encode/decode Strings + val serializedObject = mongo.toMongoValue(Color.Red).asInstanceOf[String] + serializedObject must be("Red") - // val enumValue = mongo.fromMongoValue(serializedObject) - // enumValue must be(Color.Red) + val enumValue = mongo.fromMongoValue(serializedObject) + enumValue must be(Color.Red) } } From 991644ce11f403d78073779e20e427ca0a6ab200 Mon Sep 17 00:00:00 2001 From: Peter Empen Date: Fri, 24 May 2024 18:38:13 +0200 Subject: [PATCH 030/142] toMongoValue with MongoIgnore --- .../io/sphere/mongo/generic/Derivation.scala | 15 ++++++++------- .../io/sphere/mongo/generic/MongoIgnoreSpec.scala | 15 ++++++++++----- .../io/sphere/mongo/generic/package.fmpp.scala | 2 +- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala index fb19b9b2..8007297b 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala @@ -38,13 +38,14 @@ object TypedMongoFormat: inline given derived[A](using Mirror.Of[A]): TypedMongoFormat[A] = Derivation.derived - private def addField(bson: BasicDBObject, field: Field, mongoType: MongoType) = - mongoType match - case s: SimpleMongoType => bson.put(field.name, s) - case innerBson: BasicDBObject => - if (field.embedded) innerBson.entrySet().forEach(p => bson.put(p.getKey, p.getValue)) - else bson.put(field.name, innerBson) - case MongoNothing => + private def addField(bson: BasicDBObject, field: Field, mongoType: MongoType): Unit = + if !field.ignored then + mongoType match + case s: SimpleMongoType => bson.put(field.name, s) + case innerBson: BasicDBObject => + if (field.embedded) innerBson.entrySet().forEach(p => bson.put(p.getKey, p.getValue)) + else bson.put(field.name, innerBson) + case MongoNothing => private object Derivation: import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoIgnoreSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoIgnoreSpec.scala index 08e80b71..d88b2167 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoIgnoreSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoIgnoreSpec.scala @@ -6,14 +6,14 @@ import org.scalatest.OptionValues import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec -import scala.util.chaining.* - object MongoIgnoreSpec { - private val dbo = dbObj( - "name" -> "aName" - ) + private val aName = "aName" + private val dbo = dbObj("name" -> aName) private case class MissingDefault(name: String, @MongoIgnore age: Int) + + private val defaultAge = 100 + private case class Complete(name: String, @MongoIgnore age: Int = defaultAge) } class MongoIgnoreSpec extends AnyWordSpec with Matchers with OptionValues { @@ -26,5 +26,10 @@ class MongoIgnoreSpec extends AnyWordSpec with Matchers with OptionValues { e.getMessage mustBe "Missing default parameter value for ignored field `age` on deserialization." } } + "annotated field has also a default" must { + "omit the field in serialization" in { + deriveMongoFormat[Complete].toMongoValue(Complete(aName)) mustBe dbo + } + } } } diff --git a/mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/package.fmpp.scala b/mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/package.fmpp.scala index a6476eb9..5d039f1d 100644 --- a/mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/package.fmpp.scala +++ b/mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/package.fmpp.scala @@ -260,7 +260,7 @@ package object generic extends Logging { private def typeSelector[A: ClassTag: MongoFormat](): TypeSelector[_] = { val clazz = classTag[A].runtimeClass - val (typeField, typeValue) = getMongoClassMeta(clazz).typeHint match { + val (_, typeValue) = getMongoClassMeta(clazz).typeHint match { case Some(hint) => (hint.field, hint.value) case None => (defaultTypeFieldName, defaultTypeValue(clazz)) } From 722edac6ae32d493aa16849f6a9069bf66bc4c2f Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Mon, 17 Jun 2024 11:21:39 +0200 Subject: [PATCH 031/142] Fix some Option/Null behaviour --- .../src/main/scala/io/sphere/json/package.scala | 2 +- .../scala/io/sphere/json/generic/Derivation.scala | 13 ++++--------- .../scala/io/sphere/json/JSONEmbeddedSpec.scala | 2 +- .../scala/io/sphere/json/NullHandlingSpec.scala | 8 ++++---- .../scala/io/sphere/json/OptionReaderSpec.scala | 6 +++--- .../io/sphere/json/generic/DefaultValuesSpec.scala | 4 ++-- 6 files changed, 15 insertions(+), 20 deletions(-) diff --git a/json/json-core/src/main/scala/io/sphere/json/package.scala b/json/json-core/src/main/scala/io/sphere/json/package.scala index b0ae1401..f7d59882 100644 --- a/json/json-core/src/main/scala/io/sphere/json/package.scala +++ b/json/json-core/src/main/scala/io/sphere/json/package.scala @@ -14,7 +14,7 @@ import org.json4s.jackson.compactJson /** Provides functions for reading & writing JSON, via type classes JSON/JSONR/JSONW. */ package object json extends Logging { - implicit val liftJsonFormats = DefaultFormats + implicit val liftJsonFormats: DefaultFormats = DefaultFormats type JValidation[A] = ValidatedNel[JSONError, A] diff --git a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Derivation.scala b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Derivation.scala index 9d08905d..3b62aec7 100644 --- a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Derivation.scala +++ b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Derivation.scala @@ -87,6 +87,8 @@ object JSON: else Vector(field.name) } + override val fields: Set[String] = fieldNames.toSet + override def write(value: A): JValue = val caseClassFields = value.asInstanceOf[Product].productIterator jsons @@ -113,15 +115,8 @@ object JSON: private def readField(field: Field, json: JSON[Any], jObject: JObject): JValidation[Any] = if (field.embedded) json.read(jObject) - else if (jObject.values.contains(field.fieldName) && (jObject \ field.fieldName) != JNull) - json.read(jObject \ field.fieldName) - else - field.defaultArgument match - case Some(value) => Validated.valid(value) - case None => - Validated.invalidNel( - JSONParseError( - s"Missing required field '${field.fieldName}' on deserialization.")) + else io.sphere.json.field(field.fieldName, field.defaultArgument)(jObject)(json) + end deriveCaseClass inline private def summonFormatters[T <: Tuple]: Vector[JSON[Any]] = diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala index 3f5d5483..d4cf951d 100644 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala @@ -115,7 +115,7 @@ class JSONEmbeddedSpec extends AnyWordSpec with Matchers with OptionValues { test4.subField.value.embedded.value2 mustEqual 45 } - "support the absence of optional embedded attribute" ignore { + "support the absence of optional embedded attribute" in { val json = """{ "name": "ze name" }""" val test2 = getFromJSON[Test2](json) test2.name mustEqual "ze name" diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/NullHandlingSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/NullHandlingSpec.scala index fe0faec4..5450d9e2 100644 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/NullHandlingSpec.scala +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/NullHandlingSpec.scala @@ -7,13 +7,13 @@ import org.scalatest.wordspec.AnyWordSpec class NullHandlingSpec extends AnyWordSpec with Matchers { "JSON deserialization" must { - "accept undefined fields and use default values for them" ignore { + "accept undefined fields and use default values for them" in { val jeans = getFromJSON[Jeans]("{}") jeans must be(Jeans(None, None, Set.empty, "secret")) } - "accept null values and use default values for them" ignore { + "accept null values and use default values for them" in { val jeans = getFromJSON[Jeans](""" { "leftPocket": null, @@ -26,7 +26,7 @@ class NullHandlingSpec extends AnyWordSpec with Matchers { jeans must be(Jeans(None, None, Set.empty, "secret")) } - "accept JNothing values and use default values for them" ignore { + "accept JNothing values and use default values for them" in { val jeans = getFromJValue[Jeans]( JObject( "leftPocket" -> JNothing, @@ -64,5 +64,5 @@ case class Jeans( hiddenPocket: String = "secret") object Jeans { - implicit val json: JSON[Jeans] = deriveJSON[Jeans] + given JSON[Jeans] = deriveJSON[Jeans] } diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/OptionReaderSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/OptionReaderSpec.scala index f72e0378..577c7ce2 100644 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/OptionReaderSpec.scala +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/OptionReaderSpec.scala @@ -72,7 +72,7 @@ class OptionReaderSpec extends AnyWordSpec with Matchers with OptionValues { result must be(None) } - "handle optional map" ignore { + "handle optional map" in { getFromJValue[MapClass](JObject("id" -> JLong(1L))) mustEqual MapClass(1L, None) getFromJValue[MapClass](JObject("id" -> JLong(1L), "map" -> JObject())) mustEqual @@ -90,7 +90,7 @@ class OptionReaderSpec extends AnyWordSpec with Matchers with OptionValues { JObject("id" -> JLong(1L), "map" -> JObject("a" -> JString("b"))) } - "handle optional list" ignore { + "handle optional list" in { getFromJValue[ListClass]( JObject("id" -> JLong(1L), "list" -> JArray(List(JString("hi"))))) mustEqual ListClass(1L, Some(List("hi"))) @@ -108,7 +108,7 @@ class OptionReaderSpec extends AnyWordSpec with Matchers with OptionValues { toJValue(ListClass(1L, None)) mustEqual JObject("id" -> JLong(1L), "list" -> JNothing) } - "handle absence of all fields mixed with ignored fields" ignore { + "handle absence of all fields mixed with ignored fields" in { val json = """{ "value3": "a" }""" val result = getFromJSON[Option[SimpleClass]](json) result must be(None) diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala index caa270e1..809bf3f8 100644 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala @@ -16,11 +16,11 @@ class DefaultValuesSpec extends AnyWordSpec with Matchers { test.value2 must be(None) test.value3 must be(Some("hi")) } - "handle Option with no explicit default values" ignore { + "handle Option with no explicit default values" in { val json = "{ }" val test2 = getFromJSON[Test2](json) test2.value1 must be("hello") - test2.value2 must be(None) + test2.value2 must be (None) } } } From 425bc8da80dcd8c2fce6f715b0474a1615266c6a Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Mon, 17 Jun 2024 11:44:48 +0200 Subject: [PATCH 032/142] remove warnings --- .../src/test/scala/io/sphere/json/JSONSpec.scala | 12 ++++++------ .../sphere/json/generic/JsonTypeHintFieldSpec.scala | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala index 50d2b0a7..03e6c7be 100644 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala @@ -58,7 +58,7 @@ class JSONSpec extends AnyFunSpec with Matchers { ) def read(j: JValue): ValidatedNel[JSONError, Milestone] = j match { case o: JObject => - (field[String]("name")(o), field[Option[DateTime]]("date")(o)).mapN(Milestone) + (field[String]("name")(o), field[Option[DateTime]]("date")(o)).mapN(Milestone.apply) case _ => fail("JSON object expected.") } } @@ -94,7 +94,7 @@ class JSONSpec extends AnyFunSpec with Matchers { } """ - val Invalid(errs) = fromJSON[Project](wrongTypeJSON) + val Invalid(errs) = fromJSON[Project](wrongTypeJSON): @unchecked errs.toList must equal( List( JSONFieldError(List("nr"), "JSON Number in the range of an Int expected."), @@ -122,22 +122,22 @@ class JSONSpec extends AnyFunSpec with Matchers { } it("must handle empty String") { - val Invalid(err) = fromJSON[Int]("") + val Invalid(err) = fromJSON[Int](""): @unchecked err.toList.head mustBe a[JSONParseError] } it("must provide user-friendly error by empty String") { - val Invalid(err) = fromJSON[Int]("") + val Invalid(err) = fromJSON[Int](""): @unchecked err.toList mustEqual List(JSONParseError("No content to map due to end-of-input")) } it("must handle incorrect json") { - val Invalid(err) = fromJSON[Int]("""{"key: "value"}""") + val Invalid(err) = fromJSON[Int]("""{"key: "value"}"""): @unchecked err.toList.head mustBe a[JSONParseError] } it("must provide user-friendly error by incorrect json") { - val Invalid(err) = fromJSON[Int]("""{"key: "value"}""") + val Invalid(err) = fromJSON[Int]("""{"key: "value"}"""): @unchecked err.toList mustEqual List(JSONParseError( "Unexpected character ('v' (code 118)): was expecting a colon to separate field name and value")) } diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala index 32cc5428..69d8ad99 100644 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala @@ -34,7 +34,7 @@ class JsonTypeHintFieldSpec extends AnyWordSpec with Matchers { } """ - val Valid(user) = fromJSON[UserWithPicture](json) + val Valid(user) = fromJSON[UserWithPicture](json): @unchecked user must be(UserWithPicture("foo-123", Medium, "http://example.com")) } From e5dc7f4fd70eb260ffee732dfe9eaa52bc078f19 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Mon, 17 Jun 2024 14:33:05 +0200 Subject: [PATCH 033/142] Fix sumtype writer --- .../io/sphere/json/generic/Derivation.scala | 5 +-- .../test/scala/io/sphere/json/JSONSpec.scala | 34 +++++++++---------- .../json/generic/JsonTypeHintFieldSpec.scala | 26 ++++++-------- 3 files changed, 30 insertions(+), 35 deletions(-) diff --git a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Derivation.scala b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Derivation.scala index 3b62aec7..3fcee502 100644 --- a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Derivation.scala +++ b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Derivation.scala @@ -71,8 +71,9 @@ object JSON: // we never get a trait here, only classes, it's safe to assume Product val originalTypeName = value.asInstanceOf[Product].productPrefix val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) - val json = jsonsByNames(originalTypeName).write(value) - json ++ JObject(traitMetaData.typeDiscriminator -> JString(typeName)) + val json = jsonsByNames(originalTypeName).write(value).asInstanceOf[JObject] + val typeDiscriminator = traitMetaData.typeDiscriminator -> JString(typeName) + JObject(typeDiscriminator :: json.obj) end deriveTrait diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala index 03e6c7be..b1053e7b 100644 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala @@ -142,14 +142,14 @@ class JSONSpec extends AnyFunSpec with Matchers { "Unexpected character ('v' (code 118)): was expecting a colon to separate field name and value")) } -// it("must provide derived JSON instances for sum types") { -// import io.sphere.json.generic.JSON.derived -// implicit val animalJSON = deriveJSON[Animal] -// List(Bird("Peewee"), Dog("Hasso"), Cat("Felidae")).foreach { -// a: Animal => -// fromJSON[Animal](toJSON(a)) must equal(Valid(a)) -// } -// } + it("must provide derived JSON instances for sum types") { + import io.sphere.json.generic.JSON.derived + given JSON[Animal] = deriveJSON + List(Bird("Peewee"), Dog("Hasso"), Cat("Felidae")).foreach { + animal => + fromJSON[Animal](toJSON(animal)) must equal(Valid(animal)) + } + } it("must provide derived instances for product types with concrete type parameters") { import io.sphere.json.generic.JSON.derived @@ -179,15 +179,15 @@ class JSONSpec extends AnyFunSpec with Matchers { // fromJSON[SingletonEnum](toJSON(s)) must equal(Valid(s)) // } // } -// -// it("must provide derived instances for sum types with a mix of case class / object") { -// import io.sphere.json.generic.JSON.derived -// implicit val mixedJSON: JSON[Mixed] = deriveJSON[Mixed] -// List(SingletonMixed, RecordMixed(1)).foreach { -// m: Mixed => -// fromJSON[Mixed](toJSON(m)) must equal(Valid(m)) -// } -// } + + it("must provide derived instances for sum types with a mix of case class / object") { + import io.sphere.json.generic.JSON.derived + given JSON[Mixed] = deriveJSON + List(SingletonMixed, RecordMixed(1)).foreach { + m => + fromJSON[Mixed](toJSON(m)) must equal(Valid(m)) + } + } // // it("must provide derived instances for scala.Enumeration") { // import io.sphere.json.generic.JSON.derived diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala index 69d8ad99..4658d1d0 100644 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala @@ -1,27 +1,30 @@ package io.sphere.json.generic import cats.data.Validated.Valid -import io.sphere.json._ -import org.json4s._ +import io.sphere.json.* +import org.json4s.* +import org.scalatest.Inside import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec -class JsonTypeHintFieldSpec extends AnyWordSpec with Matchers { +class JsonTypeHintFieldSpec extends AnyWordSpec with Matchers with Inside { import JsonTypeHintFieldSpec._ "JSONTypeHintField" must { - "allow to set another field to distinguish between types (toJValue)" ignore { + "allow to set another field to distinguish between types (toJValue)" in { val user = UserWithPicture("foo-123", Medium, "http://example.com") val expected = JObject( List( "userId" -> JString("foo-123"), "pictureSize" -> JObject(List("pictureType" -> JString("Medium"))), "pictureUrl" -> JString("http://example.com"))) - val _impl = implicitly[JSON[UserWithPicture]] - println(_impl) val json = toJValue[UserWithPicture](user) json must be(expected) + + inside(fromJValue[UserWithPicture](json)) { case Valid(parsedUser) => + parsedUser must be(user) + } } "allow to set another field to distinguish between types (fromJSON)" in { @@ -46,24 +49,15 @@ object JsonTypeHintFieldSpec { @JSONTypeHintField(value = "pictureType") sealed trait PictureSize - @JSONTypeHintField(value = "pictureType") case object Small extends PictureSize - @JSONTypeHintField(value = "pictureType") case object Medium extends PictureSize - @JSONTypeHintField(value = "pictureType") case object Big extends PictureSize - object PictureSize { - import io.sphere.json.generic.JSON.given - import io.sphere.json.generic.deriveJSON - implicit val json: JSON[PictureSize] = deriveJSON[PictureSize] - } - case class UserWithPicture(userId: String, pictureSize: PictureSize, pictureUrl: String) object UserWithPicture { import io.sphere.json.generic.JSON.given import io.sphere.json.generic.deriveJSON - implicit val json: JSON[UserWithPicture] = deriveJSON[UserWithPicture] + given JSON[UserWithPicture] = deriveJSON[UserWithPicture] } } From 0796166c30b9c27ea08ec914bd369a7e63760857 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Mon, 17 Jun 2024 20:45:24 +0200 Subject: [PATCH 034/142] change implicits to given --- .../test/scala/io/sphere/json/JSONEmbeddedSpec.scala | 10 +++++----- .../src/test/scala/io/sphere/json/JSONSpec.scala | 6 +++--- .../test/scala/io/sphere/json/OptionReaderSpec.scala | 12 ++++++------ .../io/sphere/json/generic/DefaultValuesSpec.scala | 4 ++-- .../scala/io/sphere/json/generic/JSONKeySpec.scala | 4 ++-- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala index d4cf951d..f5c7eba4 100644 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala @@ -10,29 +10,29 @@ object JSONEmbeddedSpec { case class Embedded(value1: String, value2: Int) object Embedded { - implicit val json: JSON[Embedded] = deriveJSON[Embedded] + given JSON[Embedded] = deriveJSON[Embedded] } case class Test1(name: String, @JSONEmbedded embedded: Embedded) object Test1 { - implicit val json: JSON[Test1] = deriveJSON[Test1] + given JSON[Test1] = deriveJSON[Test1] } case class Test2(name: String, @JSONEmbedded embedded: Option[Embedded] = None) object Test2 { - implicit val json: JSON[Test2] = deriveJSON + given JSON[Test2] = deriveJSON } case class SubTest4(@JSONEmbedded embedded: Embedded) object SubTest4 { - implicit val json: JSON[SubTest4] = deriveJSON + given JSON[SubTest4] = deriveJSON } case class Test4(subField: Option[SubTest4] = None) object Test4 { - implicit val json: JSON[Test4] = deriveJSON + given JSON[Test4] = deriveJSON } } diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala index b1053e7b..c200d031 100644 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala @@ -114,8 +114,8 @@ class JSONSpec extends AnyFunSpec with Matchers { it("must provide derived JSON instances for product types (case classes)") { import JSONSpec.{Milestone, Project} - implicit val milestoneJSON: JSON[Milestone] = deriveJSON[Milestone] - implicit val projectJSON: JSON[Project] = deriveJSON[Project] + given JSON[Milestone] = deriveJSON[Milestone] + given JSON[Project] = deriveJSON[Project] val proj = Project(42, "Linux", 7, Milestone("1.0") :: Milestone("2.0") :: Milestone("3.0") :: Nil) fromJSON[Project](toJSON(proj)) must equal(Valid(proj)) @@ -153,7 +153,7 @@ class JSONSpec extends AnyFunSpec with Matchers { it("must provide derived instances for product types with concrete type parameters") { import io.sphere.json.generic.JSON.derived - implicit val aJSON: JSON[GenericA[String]] = deriveJSON[GenericA[String]] + given JSON[GenericA[String]] = deriveJSON[GenericA[String]] val a = GenericA("hello") fromJSON[GenericA[String]](toJSON(a)) must equal(Valid(a)) } diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/OptionReaderSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/OptionReaderSpec.scala index 577c7ce2..8461d962 100644 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/OptionReaderSpec.scala +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/OptionReaderSpec.scala @@ -12,23 +12,23 @@ object OptionReaderSpec { case class SimpleClass(value1: String, value2: Int) object SimpleClass { - implicit val json: JSON[SimpleClass] = deriveJSON[SimpleClass] + given JSON[SimpleClass] = deriveJSON[SimpleClass] } case class ComplexClass(name: String, simpleClass: Option[SimpleClass]) object ComplexClass { - implicit val json: JSON[ComplexClass] = deriveJSON[ComplexClass] + given JSON[ComplexClass] = deriveJSON[ComplexClass] } case class MapClass(id: Long, map: Option[Map[String, String]]) object MapClass { - implicit val json: JSON[MapClass] = deriveJSON[MapClass] + given JSON[MapClass] = deriveJSON[MapClass] } case class ListClass(id: Long, list: Option[List[String]]) object ListClass { - implicit val json: JSON[ListClass] = deriveJSON[ListClass] + given JSON[ListClass] = deriveJSON[ListClass] } } @@ -72,7 +72,7 @@ class OptionReaderSpec extends AnyWordSpec with Matchers with OptionValues { result must be(None) } - "handle optional map" in { + "handle optional map" in { getFromJValue[MapClass](JObject("id" -> JLong(1L))) mustEqual MapClass(1L, None) getFromJValue[MapClass](JObject("id" -> JLong(1L), "map" -> JObject())) mustEqual @@ -90,7 +90,7 @@ class OptionReaderSpec extends AnyWordSpec with Matchers with OptionValues { JObject("id" -> JLong(1L), "map" -> JObject("a" -> JString("b"))) } - "handle optional list" in { + "handle optional list" in { getFromJValue[ListClass]( JObject("id" -> JLong(1L), "list" -> JArray(List(JString("hi"))))) mustEqual ListClass(1L, Some(List("hi"))) diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala index 809bf3f8..906daa53 100644 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala @@ -32,13 +32,13 @@ object DefaultValuesSpec { value3: Option[String] = Some("hi") ) object Test { - implicit val json: JSON[Test] = deriveJSON[Test] + given JSON[Test] = deriveJSON[Test] } case class Test2( value1: String = "hello", value2: Option[String] ) object Test2 { - implicit val json: JSON[Test2] = deriveJSON[Test2] + given JSON[Test2] = deriveJSON[Test2] } } diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JSONKeySpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JSONKeySpec.scala index 459755a8..df2bb582 100644 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JSONKeySpec.scala +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JSONKeySpec.scala @@ -33,7 +33,7 @@ object JSONKeySpec { @JSONKey("new_sub_value_2") value2: String ) object SubTest { - implicit val json: JSON[SubTest] = deriveJSON[SubTest] + given JSON[SubTest] = deriveJSON[SubTest] } case class Test( @@ -42,6 +42,6 @@ object JSONKeySpec { @JSONEmbedded subTest: SubTest ) object Test { - implicit val json: JSON[Test] = deriveJSON[Test] + given JSON[Test] = deriveJSON[Test] } } From 76e0073ab8b0a9b2d3cc89cfa920e2731cd80859 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Mon, 17 Jun 2024 20:51:06 +0200 Subject: [PATCH 035/142] remove forProduct spec because it was only testing the internal details of the previous implementation --- .../io/sphere/json/ForProductNSpec.scala | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 json/json-derivation-scala-3/src/test/scala/io/sphere/json/ForProductNSpec.scala diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/ForProductNSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/ForProductNSpec.scala deleted file mode 100644 index c5f92b03..00000000 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/ForProductNSpec.scala +++ /dev/null @@ -1,35 +0,0 @@ -//package io.sphere.json -// -//import java.util.UUID -// -//import io.sphere.json.ToJSONProduct._ -//import org.json4s._ -//import org.scalatest.matchers.must.Matchers -//import org.scalatest.wordspec.AnyWordSpec -// -//case class User(id: UUID, firstName: String, age: Int) -// -//class ForProductNSpec extends AnyWordSpec with Matchers { -// -// "forProductN helper methods" must { -// "serialize" in { -// implicit val encodeUser: ToJSON[User] = forProduct3(u => -// ( -// "id" -> u.id, -// "first_name" -> u.firstName, -// "age" -> u.age -// )) -// -// val id = UUID.randomUUID() -// val json = toJValue(User(id, "bidule", 109)) -// json must be( -// JObject( -// List( -// "id" -> JString(id.toString), -// "first_name" -> JString("bidule"), -// "age" -> JLong(109) -// ))) -// } -// } -// -//} From 62a6b762bbf4d6f383a441fdb48ab4feec815819 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Tue, 18 Jun 2024 17:34:55 +0200 Subject: [PATCH 036/142] Add deriveSingletonJSON --- .../json/generic/AnnotationReader.scala | 12 + .../io/sphere/json/generic/Derivation.scala | 23 +- .../sphere/json/generic/DeriveSingleton.scala | 77 +++++ .../sphere/json/DeriveSingletonJSONSpec.scala | 314 ++++++++++-------- .../test/scala/io/sphere/json/JSONSpec.scala | 2 - .../io/sphere/mongo/generic/Derivation.scala | 2 - 6 files changed, 265 insertions(+), 165 deletions(-) create mode 100644 json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/DeriveSingleton.scala diff --git a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala index c2802599..71979441 100644 --- a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala +++ b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala @@ -135,3 +135,15 @@ class AnnotationReader(using q: Quotes): '{ Map($subtypes*) } end AnnotationReader + +object AnnotationReader: + inline def readCaseClassMetaData[T]: CaseClassMetaData = ${ readCaseClassMetaDataImpl[T] } + + inline def readTraitMetaData[T]: TraitMetaData = ${ readTraitMetaDataImpl[T] } + + private def readCaseClassMetaDataImpl[T: Type](using Quotes): Expr[CaseClassMetaData] = + AnnotationReader().readCaseClassMetaData[T] + + private def readTraitMetaDataImpl[T: Type](using Quotes): Expr[TraitMetaData] = + AnnotationReader().readTraitMetaData[T] +end AnnotationReader diff --git a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Derivation.scala b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Derivation.scala index 3fcee502..d4f69c6f 100644 --- a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Derivation.scala +++ b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Derivation.scala @@ -5,10 +5,9 @@ import cats.implicits.* import io.sphere.json.{JSON, JSONParseError, JValidation} import org.json4s.DefaultJsonFormats.given import org.json4s.JsonAST.JValue -import org.json4s.{DefaultJsonFormats, JNull, JObject, JString, jvalue2monadic, jvalue2readerSyntax} +import org.json4s.{DefaultJsonFormats, JObject, JString, jvalue2monadic, jvalue2readerSyntax} import scala.deriving.Mirror -import scala.quoted.* inline def deriveJSON[A](using Mirror.Of[A]): JSON[A] = JSON.derived @@ -17,14 +16,6 @@ object JSON: inline def apply[A: JSON]: JSON[A] = summon[JSON[A]] inline given derived[A](using Mirror.Of[A]): JSON[A] = Derivation.derived[A] - private inline def readCaseClassMetaData[T]: CaseClassMetaData = ${ readCaseClassMetaDataImpl[T] } - private inline def readTraitMetaData[T]: TraitMetaData = ${ readTraitMetaDataImpl[T] } - - private def readCaseClassMetaDataImpl[T: Type](using Quotes): Expr[CaseClassMetaData] = - AnnotationReader().readCaseClassMetaData[T] - - private def readTraitMetaDataImpl[T: Type](using Quotes): Expr[TraitMetaData] = - AnnotationReader().readTraitMetaData[T] private def addField(jObject: JObject, field: Field, jValue: JValue): JValue = jValue match @@ -44,7 +35,7 @@ object JSON: inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): JSON[A] = new JSON[A]: - private val traitMetaData: TraitMetaData = readTraitMetaData[A] + private val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] private val typeHintMap: Map[String, String] = traitMetaData.subtypes.collect { case (name, classMeta) if classMeta.typeHint.isDefined => name -> classMeta.typeHint.get @@ -61,11 +52,9 @@ object JSON: case jObject: JObject => val typeName = (jObject \ traitMetaData.typeDiscriminator).as[String] val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - val parsed = jsonsByNames(originalTypeName).read(jObject) - parsed.map(_.asInstanceOf[A]) + jsonsByNames(originalTypeName).read(jObject).map(_.asInstanceOf[A]) case x => - Validated.invalidNel( - JSONParseError(s"JSON object expected. >>> trait >>> $jValue >>> ${traitMetaData}")) + Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$jValue'")) override def write(value: A): JValue = // we never get a trait here, only classes, it's safe to assume Product @@ -79,7 +68,7 @@ object JSON: inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): JSON[A] = new JSON[A]: - private val caseClassMetaData: CaseClassMetaData = readCaseClassMetaData[A] + private val caseClassMetaData: CaseClassMetaData = AnnotationReader.readCaseClassMetaData[A] private val jsons: Vector[JSON[Any]] = summonFormatters[mirrorOfProduct.MirroredElemTypes] private val fieldsAndJsons: Vector[(Field, JSON[Any])] = caseClassMetaData.fields.zip(jsons) @@ -112,7 +101,7 @@ object JSON: fieldsAsTuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) case x => - Validated.invalidNel(JSONParseError(s"JSON object expected. ${x}")) + Validated.invalidNel(JSONParseError(s"JSON object expected. $x")) private def readField(field: Field, json: JSON[Any], jObject: JObject): JValidation[Any] = if (field.embedded) json.read(jObject) diff --git a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/DeriveSingleton.scala b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/DeriveSingleton.scala new file mode 100644 index 00000000..9e4ff20b --- /dev/null +++ b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/DeriveSingleton.scala @@ -0,0 +1,77 @@ +package io.sphere.json.generic + +import cats.data.Validated +import io.sphere.json.{JSON, JSONParseError, JValidation} +import org.json4s.{JNull, JString, JValue} + +import scala.deriving.Mirror + +inline def deriveSingletonJSON[A](using Mirror.Of[A]): JSON[A] = DeriveSingleton.derived + +object DeriveSingleton { + + inline given derived[A](using Mirror.Of[A]): JSON[A] = Derivation.derived[A] + + private object Derivation { + + import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} + + inline def derived[A](using m: Mirror.Of[A]): JSON[A] = + inline m match + case s: Mirror.SumOf[A] => deriveTrait(s) + case p: Mirror.ProductOf[A] => deriveObject(p) + + inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): JSON[A] = + new JSON[A]: + private val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] + private val typeHintMap: Map[String, String] = traitMetaData.subtypes.collect { + case (name, classMeta) if classMeta.typeHint.isDefined => + name -> classMeta.typeHint.get + } + private val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) + private val jsons: Seq[JSON[Any]] = summonFormatters[mirrorOfSum.MirroredElemTypes] + private val names: Seq[String] = + constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector + .asInstanceOf[Vector[String]] + private val jsonsByNames: Map[String, JSON[Any]] = names.zip(jsons).toMap + + override def read(jValue: JValue): JValidation[A] = + jValue match + case JString(typeName) => + val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) + jsonsByNames.get(originalTypeName) match + case Some(json) => + json.read(JNull).map(_.asInstanceOf[A]) + case None => + Validated.invalidNel(JSONParseError(s"'$typeName' is not a valid value")) + + case x => + Validated.invalidNel(JSONParseError(s"JSON string expected. Got >>> $jValue")) + + override def write(value: A): JValue = + val originalTypeName = value.asInstanceOf[Product].productPrefix + val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) + JString(typeName) + + end deriveTrait + + inline private def deriveObject[A](mirrorOfProduct: Mirror.ProductOf[A]): JSON[A] = + new JSON[A]: + override def write(value: A): JValue = ??? // This is already taken care of in `deriveTrait` + override def read(jValue: JValue): JValidation[A] = + // Just create the object instance, no need to do anything else + val tuple = Tuple.fromArray(Array.empty[Any]) + val obj = mirrorOfProduct.fromTuple(tuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) + Validated.Valid(obj) + end deriveObject + + inline private def summonFormatters[T <: Tuple]: Vector[JSON[Any]] = + inline erasedValue[T] match + case _: EmptyTuple => Vector.empty + case _: (t *: ts) => + summonInline[JSON[t]] + .asInstanceOf[JSON[Any]] +: summonFormatters[ts] + + } + +} diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala index 99504458..7726a865 100644 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala @@ -1,144 +1,170 @@ -//package io.sphere.json -//import io.sphere.json.generic.* -//import io.sphere.json.generic.JSON.derived -//import org.json4s.JValue -//import org.scalatest.matchers.must.Matchers -//import org.scalatest.wordspec.AnyWordSpec -// -//class DeriveSingletonJSONSpec extends AnyWordSpec with Matchers { -// "DeriveSingletonJSON" must { -// "read normal singleton values" in { -// val user = getFromJSON[UserWithPicture](""" -// { -// "userId": "foo-123", -// "pictureSize": "Medium", -// "pictureUrl": "http://exmple.com" -// } -// """) -// -// user must be(UserWithPicture("foo-123", Medium, "http://exmple.com")) -// } -// -// "fail to read if singleton value is unknown" in { -// a[JSONException] must be thrownBy getFromJSON[UserWithPicture](""" -// { -// "userId": "foo-123", -// "pictureSize": "foo", -// "pictureUrl": "http://exmple.com" -// } -// """) -// } -// "write normal singleton values" in { -// val userJson = toJValue(UserWithPicture("foo-123", Medium, "http://exmple.com")) -// -// val Valid(expectedJson) = parseJSON(""" -// { -// "userId": "foo-123", -// "pictureSize": "Medium", -// "pictureUrl": "http://exmple.com" -// } -// """) -// -// filter(userJson) must be(expectedJson) -// } -// -// "read custom singleton values" in { -// val user = getFromJSON[UserWithPicture](""" -// { -// "userId": "foo-123", -// "pictureSize": "bar", -// "pictureUrl": "http://exmple.com" -// } -// """) -// -// user must be(UserWithPicture("foo-123", Custom, "http://exmple.com")) -// } -// -// "write custom singleton values" in { -// val userJson = toJValue(UserWithPicture("foo-123", Custom, "http://exmple.com")) -// -// val Valid(expectedJson) = parseJSON(""" -// { -// "userId": "foo-123", -// "pictureSize": "bar", -// "pictureUrl": "http://exmple.com" -// } -// """) -// -// filter(userJson) must be(expectedJson) -// } -// -// "write and consequently read, which must produce the original value" in { -// val originalUser = UserWithPicture("foo-123", Medium, "http://exmple.com") -// val newUser = getFromJSON[UserWithPicture](compact(render(toJValue(originalUser)))) -// -// newUser must be(originalUser) -// } -// -// "read and write sealed trait with only one subtype" in { -// val json = -// """ -// { -// "userId": "foo-123", -// "pictureSize": "Medium", -// "pictureUrl": "http://example.com", -// "access": { -// "type": "Authorized", -// "project": "internal" -// } -// } -// """ -// val user = getFromJSON[UserWithPicture](json) -// -// user must be( -// UserWithPicture( -// "foo-123", -// Medium, -// "http://example.com", -// Some(Access.Authorized("internal")))) -// -// val newJson = toJValue[UserWithPicture](user) -// Valid(newJson) must be(parseJSON(json)) -// -// val Valid(newUser) = fromJValue[UserWithPicture](newJson) -// newUser must be(user) -// } -// } -// -// private def filter(jvalue: JValue): JValue = -// jvalue.removeField { -// case (_, JNothing) => true -// case _ => false -// } -//} -// -//sealed abstract class PictureSize(val weight: Int, val height: Int) -// -//case object Small extends PictureSize(100, 100) {} -//case object Medium extends PictureSize(500, 450) -//case object Big extends PictureSize(1024, 2048) -// -//@JSONTypeHint("bar") -//case object Custom extends PictureSize(1, 2) -// -////object PictureSize { -//// implicit val json: JSON[PictureSize] = deriveJSON[PictureSize] -////} -// -////sealed trait Access -////object Access { -//// // only one sub-type -//// case class Authorized(project: String) extends Access -//// -//// implicit val json: JSON[Access] = deriveJSON[Access] -////} -// -//case class UserWithPicture( -// userId: String, -// pictureSize: PictureSize, -// pictureUrl: String -// /*access: Option[Access] = None*/ ) -// -//object UserWithPicture { -// implicit val json: JSON[UserWithPicture] = deriveJSON[UserWithPicture] -//} +package io.sphere.json + +import cats.data.Validated.Valid +import io.sphere.json.generic.* +import org.json4s.DefaultJsonFormats.given +import org.json4s.{DynamicJValueImplicits, JArray, JObject, JValue} +import org.json4s.JsonAST.{JField, JNothing} +import org.json4s.jackson.JsonMethods.{compact, render} +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class DeriveSingletonJSONSpec extends AnyWordSpec with Matchers { + "DeriveSingletonJSON" must { + "read normal singleton values" in { + val user = getFromJSON[UserWithPicture](""" + { + "userId": "foo-123", + "pictureSize": "Medium", + "pictureUrl": "http://exmple.com" + } + """) + + user must be(UserWithPicture("foo-123", Medium, "http://exmple.com")) + } + + "fail to read if singleton value is unknown" in { + a[JSONException] must be thrownBy getFromJSON[UserWithPicture](""" + { + "userId": "foo-123", + "pictureSize": "foo", + "pictureUrl": "http://exmple.com" + } + """) + } + + "write normal singleton values" in { + val userJson = toJValue(UserWithPicture("foo-123", Medium, "http://exmple.com")) + + val Valid(expectedJson) = parseJSON(""" + { + "userId": "foo-123", + "pictureSize": "Medium", + "pictureUrl": "http://exmple.com" + } + """): @unchecked + + filter(userJson) must be(expectedJson) + } + + "read custom singleton values" in { + val user = getFromJSON[UserWithPicture](""" + { + "userId": "foo-123", + "pictureSize": "bar", + "pictureUrl": "http://exmple.com" + } + """) + + user must be(UserWithPicture("foo-123", Custom, "http://exmple.com")) + } + + "write custom singleton values" in { + val userJson = toJValue(UserWithPicture("foo-123", Custom, "http://exmple.com")) + + val Valid(expectedJson) = parseJSON(""" + { + "userId": "foo-123", + "pictureSize": "bar", + "pictureUrl": "http://exmple.com" + } + """): @unchecked + + filter(userJson) must be(expectedJson) + } + + "write and consequently read, which must produce the original value" in { + val originalUser = UserWithPicture("foo-123", Medium, "http://exmple.com") + val newUser = getFromJSON[UserWithPicture](compact(render(toJValue(originalUser)))) + + newUser must be(originalUser) + } + + "read and write sealed trait with only one subtype" in { + val json = """ + { + "userId": "foo-123", + "pictureSize": "Medium", + "pictureUrl": "http://example.com", + "access": { + "type": "Authorized", + "project": "internal" + } + } + """ + val user = getFromJSON[UserWithPicture](json) + + user must be( + UserWithPicture( + "foo-123", + Medium, + "http://example.com", + Some(Access.Authorized("internal")))) + + val newJson = toJValue[UserWithPicture](user) + Valid(newJson) must be(parseJSON(json)) + + val Valid(newUser) = fromJValue[UserWithPicture](newJson): @unchecked + newUser must be(user) + } + } + + private def filter(jvalue: JValue): JValue = + jvalue.removeField { + case (_, JNothing) => true + case _ => false + } + + extension (jv: JValue) + def removeField(p: JField => Boolean): JValue = jv.transform { case JObject(l) => + JObject(l.filterNot(p)) + } + + def transform(f: PartialFunction[JValue, JValue]): JValue = map { x => + f.applyOrElse[JValue, JValue](x, _ => x) + } + + def map(f: JValue => JValue): JValue = { + def rec(v: JValue): JValue = v match { + case JObject(l) => f(JObject(l.map { case (n, va) => (n, rec(va)) })) + case JArray(l) => f(JArray(l.map(rec))) + case x => f(x) + } + rec(jv) + } + +} + +sealed abstract class PictureSize(val weight: Int, val height: Int) + +case object Small extends PictureSize(100, 100) +case object Medium extends PictureSize(500, 450) +case object Big extends PictureSize(1024, 2048) + +@JSONTypeHint("bar") +case object Custom extends PictureSize(1, 2) + +object PictureSize { + import DeriveSingleton.derived + + given JSON[PictureSize] = deriveSingletonJSON +} + +sealed trait Access +object Access { + // only one sub-type + import JSON.derived + case class Authorized(project: String) extends Access + + given JSON[Access] = deriveJSON +} + +case class UserWithPicture( + userId: String, + pictureSize: PictureSize, + pictureUrl: String, + access: Option[Access] = None) + +object UserWithPicture { + given JSON[UserWithPicture] = deriveJSON[UserWithPicture] +} diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala index c200d031..11122112 100644 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala @@ -152,14 +152,12 @@ class JSONSpec extends AnyFunSpec with Matchers { } it("must provide derived instances for product types with concrete type parameters") { - import io.sphere.json.generic.JSON.derived given JSON[GenericA[String]] = deriveJSON[GenericA[String]] val a = GenericA("hello") fromJSON[GenericA[String]](toJSON(a)) must equal(Valid(a)) } it("must provide derived instances for product types with generic type parameters") { - import io.sphere.json.generic.JSON.derived implicit def aJSON[A: JSON]: JSON[GenericA[A]] = deriveJSON[GenericA[A]] val a = GenericA("hello") fromJSON[GenericA[String]](toJSON(a)) must equal(Valid(a)) diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala index 8007297b..ecac484c 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala @@ -1,13 +1,11 @@ package io.sphere.mongo.generic import com.mongodb.BasicDBObject -import org.bson.BSONObject import org.bson.types.ObjectId import java.util.UUID import java.util.regex.Pattern import scala.deriving.Mirror -import scala.quoted.* object MongoNothing type SimpleMongoType = UUID | String | ObjectId | Short | Int | Long | Float | Double | Boolean | From 029447795152ceda977db4a6d12c131114e5c274 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Mon, 1 Jul 2024 12:27:06 +0200 Subject: [PATCH 037/142] sphere-json-core now compiles with Scala3 --- .../test/scala/io/sphere/json/JSONProperties.scala | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/json/json-core/src/test/scala/io/sphere/json/JSONProperties.scala b/json/json-core/src/test/scala/io/sphere/json/JSONProperties.scala index 52e3b8a6..415c3290 100644 --- a/json/json-core/src/test/scala/io/sphere/json/JSONProperties.scala +++ b/json/json-core/src/test/scala/io/sphere/json/JSONProperties.scala @@ -74,25 +74,25 @@ object JSONProperties extends Properties("JSON") { least <- Arbitrary.arbitrary[Long] } yield new UUID(most, least)) - implicit val currencyEqual = new Eq[Currency] { + implicit val currencyEqual: Eq[Currency] = new Eq[Currency] { def eqv(c1: Currency, c2: Currency) = c1.getCurrencyCode == c2.getCurrencyCode } - implicit val localeEqual = new Eq[Locale] { + implicit val localeEqual: Eq[Locale] = new Eq[Locale] { def eqv(l1: Locale, l2: Locale) = l1.toLanguageTag == l2.toLanguageTag } - implicit val moneyEqual = new Eq[Money] { + implicit val moneyEqual: Eq[Money] = new Eq[Money] { override def eqv(x: Money, y: Money): Boolean = x == y } - implicit val dateTimeEqual = new Eq[DateTime] { + implicit val dateTimeEqual: Eq[DateTime] = new Eq[DateTime] { def eqv(dt1: DateTime, dt2: DateTime) = dt1 == dt2 } - implicit val localTimeEqual = new Eq[LocalTime] { + implicit val localTimeEqual: Eq[LocalTime] = new Eq[LocalTime] { def eqv(dt1: LocalTime, dt2: LocalTime) = dt1 == dt2 } - implicit val localDateEqual = new Eq[LocalDate] { + implicit val localDateEqual: Eq[LocalDate] = new Eq[LocalDate] { def eqv(dt1: LocalDate, dt2: LocalDate) = dt1 == dt2 } - implicit val yearMonthEqual = new Eq[YearMonth] { + implicit val yearMonthEqual: Eq[YearMonth] = new Eq[YearMonth] { def eqv(dt1: YearMonth, dt2: YearMonth) = dt1 == dt2 } From 7bf1515ea0fc64a1708562b1fe4ec2d8fd3a6636 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 19 Jul 2024 08:56:18 +0200 Subject: [PATCH 038/142] Turn off indentation based syntax. --- .scalafmt.conf | 1 + build.sbt | 5 +- .../json/generic/AnnotationReader.scala | 26 ++-- .../io/sphere/json/generic/Derivation.scala | 35 ++++-- .../sphere/json/generic/DeriveSingleton.scala | 29 +++-- .../sphere/json/DeriveSingletonJSONSpec.scala | 5 +- .../test/scala/io/sphere/json/JSONSpec.scala | 10 +- .../json/generic/DefaultValuesSpec.scala | 2 +- .../MongoFormatCatsInstancesTest.scala | 6 + .../mongo/generic/AnnotationReader.scala | 19 +-- .../mongo/generic/DefaultMongoFormats.scala | 13 +- .../io/sphere/mongo/generic/Derivation.scala | 113 ++++++++++-------- .../io/sphere/mongo/DerivationSpec.scala | 6 +- .../io/sphere/mongo/SerializationTest.scala | 22 ++-- .../mongo/generic/DefaultValuesSpec.scala | 13 +- 15 files changed, 179 insertions(+), 126 deletions(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index ec9f8145..e9c98a85 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,6 +1,7 @@ version = 3.7.17 runner.dialect = scala3 +runner.dialectOverride.allowSignificantIndentation = false maxColumn = 100 diff --git a/build.sbt b/build.sbt index bdd207d5..1c5730d2 100644 --- a/build.sbt +++ b/build.sbt @@ -44,7 +44,8 @@ lazy val standardSettings = Defaults.coreDefaultSettings ++ Seq( scalacOptions ++= Seq( "-deprecation", "-unchecked", - "-feature" + "-feature", + "-noindent" ), javacOptions ++= Seq("-deprecation", "-Xlint:unchecked"), // targets Java 8 bytecode (scalac & javac) @@ -116,12 +117,12 @@ lazy val `sphere-json` = project lazy val `sphere-mongo-core` = project .in(file("./mongo/mongo-core")) + .settings(crossScalaVersions := Seq(scala3, scala2_13)) .settings(standardSettings: _*) .dependsOn(`sphere-util`) lazy val `sphere-mongo-derivation` = project .in(file("./mongo/mongo-derivation")) - .settings(crossScalaVersions := Seq(scala2_12, scala2_13)) .settings(standardSettings: _*) .settings(Fmpp.settings: _*) .dependsOn(`sphere-mongo-core`) diff --git a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala index 71979441..69c64576 100644 --- a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala +++ b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala @@ -33,7 +33,8 @@ case class TraitMetaData( val typeDiscriminator: String = typeHintFieldRaw.map(_.value).getOrElse("type") } -class AnnotationReader(using q: Quotes): +class AnnotationReader(using q: Quotes) { + import q.reflect.* def readCaseClassMetaData[T: Type]: Expr[CaseClassMetaData] = { @@ -41,7 +42,7 @@ class AnnotationReader(using q: Quotes): caseClassMetaData(sym) } - def readTraitMetaData[T: Type]: Expr[TraitMetaData] = + def readTraitMetaData[T: Type]: Expr[TraitMetaData] = { val sym = TypeRepr.of[T].typeSymbol val typeHintField = sym.annotations.map(findJSONTypeHintField).find(_.isDefined).flatten match { @@ -56,6 +57,7 @@ class AnnotationReader(using q: Quotes): subtypes = ${ subtypeAnnotations(sym) } ) } + } private def annotationTree(tree: Tree): Option[Expr[MA]] = Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[MA]).map(_.asExprOf[MA]) @@ -81,7 +83,7 @@ class AnnotationReader(using q: Quotes): .filter(_.isExprOf[JSONTypeHintField]) .map(_.asExprOf[JSONTypeHintField]) - private def collectFieldInfo(companion: Symbol)(s: Symbol, paramIdx: Int): Expr[Field] = + private def collectFieldInfo(companion: Symbol)(s: Symbol, paramIdx: Int): Expr[Field] = { val embedded = Expr(s.annotations.exists(findEmbedded)) val ignored = Expr(s.annotations.exists(findIgnored)) val name = Expr(s.name) @@ -105,8 +107,9 @@ class AnnotationReader(using q: Quotes): jsonKey = $key, defaultArgument = $defArgOpt) } + } - private def caseClassMetaData(sym: Symbol): Expr[CaseClassMetaData] = + private def caseClassMetaData(sym: Symbol): Expr[CaseClassMetaData] = { val caseParams = sym.primaryConstructor.paramSymss.take(1).flatten val fields = Varargs(caseParams.zipWithIndex.map(collectFieldInfo(sym.companionModule))) val name = Expr(sym.name) @@ -122,21 +125,22 @@ class AnnotationReader(using q: Quotes): fields = Vector($fields*) ) } - end caseClassMetaData + } - private def subtypeAnnotation(sym: Symbol): Expr[(String, CaseClassMetaData)] = + private def subtypeAnnotation(sym: Symbol): Expr[(String, CaseClassMetaData)] = { val name = Expr(sym.name) val annots = caseClassMetaData(sym) '{ ($name, $annots) } - end subtypeAnnotation + } - private def subtypeAnnotations(sym: Symbol): Expr[Map[String, CaseClassMetaData]] = + private def subtypeAnnotations(sym: Symbol): Expr[Map[String, CaseClassMetaData]] = { val subtypes = Varargs(sym.children.map(subtypeAnnotation)) '{ Map($subtypes*) } + } -end AnnotationReader +} -object AnnotationReader: +object AnnotationReader { inline def readCaseClassMetaData[T]: CaseClassMetaData = ${ readCaseClassMetaDataImpl[T] } inline def readTraitMetaData[T]: TraitMetaData = ${ readTraitMetaDataImpl[T] } @@ -146,4 +150,4 @@ object AnnotationReader: private def readTraitMetaDataImpl[T: Type](using Quotes): Expr[TraitMetaData] = AnnotationReader().readTraitMetaData[T] -end AnnotationReader +} diff --git a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Derivation.scala b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Derivation.scala index d4f69c6f..67a26666 100644 --- a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Derivation.scala +++ b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Derivation.scala @@ -11,30 +11,32 @@ import scala.deriving.Mirror inline def deriveJSON[A](using Mirror.Of[A]): JSON[A] = JSON.derived -object JSON: +object JSON { private val emptyFieldsSet: Vector[String] = Vector.empty inline def apply[A: JSON]: JSON[A] = summon[JSON[A]] inline given derived[A](using Mirror.Of[A]): JSON[A] = Derivation.derived[A] private def addField(jObject: JObject, field: Field, jValue: JValue): JValue = - jValue match + jValue match { case o: JObject => if (field.embedded) JObject(jObject.obj ++ o.obj) else JObject(jObject.obj :+ (field.fieldName -> o)) case other => JObject(jObject.obj :+ (field.fieldName -> other)) + } - private object Derivation: + private object Derivation { import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} inline def derived[A](using m: Mirror.Of[A]): JSON[A] = - inline m match + inline m match { case s: Mirror.SumOf[A] => deriveTrait(s) case p: Mirror.ProductOf[A] => deriveCaseClass(p) + } inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): JSON[A] = - new JSON[A]: + new JSON[A] { private val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] private val typeHintMap: Map[String, String] = traitMetaData.subtypes.collect { case (name, classMeta) if classMeta.typeHint.isDefined => @@ -48,26 +50,28 @@ object JSON: private val jsonsByNames: Map[String, JSON[Any]] = names.zip(jsons).toMap override def read(jValue: JValue): JValidation[A] = - jValue match + jValue match { case jObject: JObject => val typeName = (jObject \ traitMetaData.typeDiscriminator).as[String] val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) jsonsByNames(originalTypeName).read(jObject).map(_.asInstanceOf[A]) case x => Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$jValue'")) + } - override def write(value: A): JValue = + override def write(value: A): JValue = { // we never get a trait here, only classes, it's safe to assume Product val originalTypeName = value.asInstanceOf[Product].productPrefix val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) val json = jsonsByNames(originalTypeName).write(value).asInstanceOf[JObject] val typeDiscriminator = traitMetaData.typeDiscriminator -> JString(typeName) JObject(typeDiscriminator :: json.obj) + } - end deriveTrait + } inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): JSON[A] = - new JSON[A]: + new JSON[A] { private val caseClassMetaData: CaseClassMetaData = AnnotationReader.readCaseClassMetaData[A] private val jsons: Vector[JSON[Any]] = summonFormatters[mirrorOfProduct.MirroredElemTypes] private val fieldsAndJsons: Vector[(Field, JSON[Any])] = caseClassMetaData.fields.zip(jsons) @@ -79,7 +83,7 @@ object JSON: override val fields: Set[String] = fieldNames.toSet - override def write(value: A): JValue = + override def write(value: A): JValue = { val caseClassFields = value.asInstanceOf[Product].productIterator jsons .zip(caseClassFields) @@ -87,9 +91,10 @@ object JSON: .foldLeft[JValue](JObject()) { case (jObject, ((json, fieldValue), field)) => addField(jObject.asInstanceOf[JObject], field, json.write(fieldValue)) } + } override def read(jValue: JValue): JValidation[A] = - jValue match + jValue match { case jObject: JObject => for { fieldsAsAList <- fieldsAndJsons @@ -102,16 +107,20 @@ object JSON: case x => Validated.invalidNel(JSONParseError(s"JSON object expected. $x")) + } private def readField(field: Field, json: JSON[Any], jObject: JObject): JValidation[Any] = if (field.embedded) json.read(jObject) else io.sphere.json.field(field.fieldName, field.defaultArgument)(jObject)(json) - end deriveCaseClass + } inline private def summonFormatters[T <: Tuple]: Vector[JSON[Any]] = - inline erasedValue[T] match + inline erasedValue[T] match { case _: EmptyTuple => Vector.empty case _: (t *: ts) => summonInline[JSON[t]] .asInstanceOf[JSON[Any]] +: summonFormatters[ts] + } + } +} diff --git a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/DeriveSingleton.scala b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/DeriveSingleton.scala index 9e4ff20b..2b65c923 100644 --- a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/DeriveSingleton.scala +++ b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/DeriveSingleton.scala @@ -17,12 +17,13 @@ object DeriveSingleton { import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} inline def derived[A](using m: Mirror.Of[A]): JSON[A] = - inline m match + inline m match { case s: Mirror.SumOf[A] => deriveTrait(s) case p: Mirror.ProductOf[A] => deriveObject(p) + } inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): JSON[A] = - new JSON[A]: + new JSON[A] { private val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] private val typeHintMap: Map[String, String] = traitMetaData.subtypes.collect { case (name, classMeta) if classMeta.typeHint.isDefined => @@ -36,42 +37,46 @@ object DeriveSingleton { private val jsonsByNames: Map[String, JSON[Any]] = names.zip(jsons).toMap override def read(jValue: JValue): JValidation[A] = - jValue match + jValue match { case JString(typeName) => val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - jsonsByNames.get(originalTypeName) match + jsonsByNames.get(originalTypeName) match { case Some(json) => json.read(JNull).map(_.asInstanceOf[A]) case None => Validated.invalidNel(JSONParseError(s"'$typeName' is not a valid value")) + } case x => Validated.invalidNel(JSONParseError(s"JSON string expected. Got >>> $jValue")) + } - override def write(value: A): JValue = + override def write(value: A): JValue = { val originalTypeName = value.asInstanceOf[Product].productPrefix val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) JString(typeName) + } - end deriveTrait + } inline private def deriveObject[A](mirrorOfProduct: Mirror.ProductOf[A]): JSON[A] = - new JSON[A]: + new JSON[A] { override def write(value: A): JValue = ??? // This is already taken care of in `deriveTrait` - override def read(jValue: JValue): JValidation[A] = + + override def read(jValue: JValue): JValidation[A] = { // Just create the object instance, no need to do anything else val tuple = Tuple.fromArray(Array.empty[Any]) val obj = mirrorOfProduct.fromTuple(tuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) Validated.Valid(obj) - end deriveObject + } + } inline private def summonFormatters[T <: Tuple]: Vector[JSON[Any]] = - inline erasedValue[T] match + inline erasedValue[T] match { case _: EmptyTuple => Vector.empty case _: (t *: ts) => summonInline[JSON[t]] .asInstanceOf[JSON[Any]] +: summonFormatters[ts] - + } } - } diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala index 7726a865..dc334403 100644 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala @@ -115,7 +115,7 @@ class DeriveSingletonJSONSpec extends AnyWordSpec with Matchers { case _ => false } - extension (jv: JValue) + extension (jv: JValue) { def removeField(p: JField => Boolean): JValue = jv.transform { case JObject(l) => JObject(l.filterNot(p)) } @@ -130,9 +130,10 @@ class DeriveSingletonJSONSpec extends AnyWordSpec with Matchers { case JArray(l) => f(JArray(l.map(rec))) case x => f(x) } + rec(jv) } - + } } sealed abstract class PictureSize(val weight: Int, val height: Int) diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala index 11122112..240ac465 100644 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala @@ -145,9 +145,8 @@ class JSONSpec extends AnyFunSpec with Matchers { it("must provide derived JSON instances for sum types") { import io.sphere.json.generic.JSON.derived given JSON[Animal] = deriveJSON - List(Bird("Peewee"), Dog("Hasso"), Cat("Felidae")).foreach { - animal => - fromJSON[Animal](toJSON(animal)) must equal(Valid(animal)) + List(Bird("Peewee"), Dog("Hasso"), Cat("Felidae")).foreach { animal => + fromJSON[Animal](toJSON(animal)) must equal(Valid(animal)) } } @@ -181,9 +180,8 @@ class JSONSpec extends AnyFunSpec with Matchers { it("must provide derived instances for sum types with a mix of case class / object") { import io.sphere.json.generic.JSON.derived given JSON[Mixed] = deriveJSON - List(SingletonMixed, RecordMixed(1)).foreach { - m => - fromJSON[Mixed](toJSON(m)) must equal(Valid(m)) + List(SingletonMixed, RecordMixed(1)).foreach { m => + fromJSON[Mixed](toJSON(m)) must equal(Valid(m)) } } // diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala index 906daa53..7bca45fe 100644 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala +++ b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala @@ -20,7 +20,7 @@ class DefaultValuesSpec extends AnyWordSpec with Matchers { val json = "{ }" val test2 = getFromJSON[Test2](json) test2.value1 must be("hello") - test2.value2 must be (None) + test2.value2 must be(None) } } } diff --git a/mongo/mongo-core/src/test/scala/io/sphere/mongo/catsinstances/MongoFormatCatsInstancesTest.scala b/mongo/mongo-core/src/test/scala/io/sphere/mongo/catsinstances/MongoFormatCatsInstancesTest.scala index 47f37647..99b7b4b8 100644 --- a/mongo/mongo-core/src/test/scala/io/sphere/mongo/catsinstances/MongoFormatCatsInstancesTest.scala +++ b/mongo/mongo-core/src/test/scala/io/sphere/mongo/catsinstances/MongoFormatCatsInstancesTest.scala @@ -1,6 +1,7 @@ package io.sphere.mongo.catsinstances import cats.syntax.invariant._ +import io.sphere.mongo.Test import io.sphere.mongo.format.DefaultMongoFormats._ import io.sphere.mongo.format._ import org.scalatest.matchers.must.Matchers @@ -18,6 +19,11 @@ class MongoFormatCatsInstancesTest extends AnyWordSpec with Matchers { myNewId must be(myId) } } + + "asd" in { + println("asdd") + Test.fn + } } object MongoFormatCatsInstancesTest { diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala index ccca706b..7ac57f36 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala @@ -48,7 +48,7 @@ object AnnotationReader { AnnotationReader().readTraitMetaData[T] } -class AnnotationReader(using q: Quotes): +class AnnotationReader(using q: Quotes) { import q.reflect.* def readCaseClassMetaData[T: Type]: Expr[CaseClassMetaData] = { @@ -97,7 +97,7 @@ class AnnotationReader(using q: Quotes): .filter(_.isExprOf[MongoTypeHintField]) .map(_.asExprOf[MongoTypeHintField]) - private def collectFieldInfo(companion: Symbol)(s: Symbol, paramIdx: Int): Expr[Field] = + private def collectFieldInfo(companion: Symbol)(s: Symbol, paramIdx: Int): Expr[Field] = { val embedded = Expr(s.annotations.exists(findEmbedded)) val ignored = Expr(s.annotations.exists(findIgnored)) val name = Expr(s.name) @@ -121,8 +121,9 @@ class AnnotationReader(using q: Quotes): mongoKey = $mongoKey, defaultArgument = $defArgOpt) } + } - private def caseClassMetaData(sym: Symbol): Expr[CaseClassMetaData] = + private def caseClassMetaData(sym: Symbol): Expr[CaseClassMetaData] = { val caseParams = sym.primaryConstructor.paramSymss.take(1).flatten val fields = Varargs(caseParams.zipWithIndex.map(collectFieldInfo(sym.companionModule))) val name = Expr(sym.name) @@ -138,16 +139,16 @@ class AnnotationReader(using q: Quotes): fields = Vector($fields*) ) } - end caseClassMetaData + } - private def subtypeAnnotation(sym: Symbol): Expr[(String, CaseClassMetaData)] = + private def subtypeAnnotation(sym: Symbol): Expr[(String, CaseClassMetaData)] = { val name = Expr(sym.name) val annots = caseClassMetaData(sym) '{ ($name, $annots) } - end subtypeAnnotation + } - private def subtypeAnnotations(sym: Symbol): Expr[Map[String, CaseClassMetaData]] = + private def subtypeAnnotations(sym: Symbol): Expr[Map[String, CaseClassMetaData]] = { val subtypes = Varargs(sym.children.map(subtypeAnnotation)) '{ Map($subtypes*) } - -end AnnotationReader + } +} diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/DefaultMongoFormats.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/DefaultMongoFormats.scala index b6c16b88..8b386cea 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/DefaultMongoFormats.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/DefaultMongoFormats.scala @@ -12,24 +12,27 @@ trait DefaultMongoFormats { given TypedMongoFormat[Boolean] = new NativeMongoFormat[Boolean] given [A](using TypedMongoFormat[A]): TypedMongoFormat[Option[A]] = - new TypedMongoFormat[Option[A]]: + new TypedMongoFormat[Option[A]] { override def toMongoValue(a: Option[A]): MongoType = - a match + a match { case Some(value) => summon[TypedMongoFormat[A]].toMongoValue(value) case None => MongoNothing + } - override def fromMongoValue(mongoType: MongoType): Option[A] = + override def fromMongoValue(mongoType: MongoType): Option[A] = { val fieldNames = summon[TypedMongoFormat[A]].fieldNames if (mongoType == null) None else - mongoType match + mongoType match { case s: SimpleMongoType => Some(summon[TypedMongoFormat[A]].fromMongoValue(s)) case bson: BasicDBObject => val bsonFieldNames = bson.keySet().toArray if (fieldNames.nonEmpty && bsonFieldNames.intersect(fieldNames).isEmpty) None else Some(summon[TypedMongoFormat[A]].fromMongoValue(bson)) case MongoNothing => None // This can't happen, but it makes the compiler happy + } + } override def default: Option[Option[A]] = Some(None) - + } } diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala index ecac484c..67adad5f 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala +++ b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala @@ -29,7 +29,7 @@ private final class NativeMongoFormat[A <: SimpleMongoType] extends TypedMongoFo inline def deriveMongoFormat[A](using Mirror.Of[A]): TypedMongoFormat[A] = TypedMongoFormat.derived -object TypedMongoFormat: +object TypedMongoFormat { inline def apply[A: TypedMongoFormat]: TypedMongoFormat[A] = summon private val emptyFieldsSet: Vector[String] = Vector.empty @@ -37,24 +37,27 @@ object TypedMongoFormat: inline given derived[A](using Mirror.Of[A]): TypedMongoFormat[A] = Derivation.derived private def addField(bson: BasicDBObject, field: Field, mongoType: MongoType): Unit = - if !field.ignored then - mongoType match + if (!field.ignored) + mongoType match { case s: SimpleMongoType => bson.put(field.name, s) case innerBson: BasicDBObject => if (field.embedded) innerBson.entrySet().forEach(p => bson.put(p.getKey, p.getValue)) else bson.put(field.name, innerBson) case MongoNothing => + } + + private object Derivation { - private object Derivation: import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} inline def derived[A](using m: Mirror.Of[A]): TypedMongoFormat[A] = - inline m match + inline m match { case s: Mirror.SumOf[A] => deriveTrait(s) case p: Mirror.ProductOf[A] => deriveCaseClass(p) + } inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): TypedMongoFormat[A] = - new TypedMongoFormat[A]: + new TypedMongoFormat[A] { private val traitMetaData = AnnotationReader.readTraitMetaData[A] private val typeHintMap = traitMetaData.subtypes.collect { case (name, classMeta) if classMeta.typeHint.isDefined => @@ -66,7 +69,7 @@ object TypedMongoFormat: .asInstanceOf[Vector[String]] private val formattersByTypeName = names.zip(formatters).toMap - override def toMongoValue(a: A): MongoType = + override def toMongoValue(a: A): MongoType = { // we never get a trait here, only classes, it's safe to assume Product val originalTypeName = a.asInstanceOf[Product].productPrefix val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) @@ -74,68 +77,76 @@ object TypedMongoFormat: formattersByTypeName(originalTypeName).toMongoValue(a).asInstanceOf[BasicDBObject] bson.put(traitMetaData.typeDiscriminator, typeName) bson + } override def fromMongoValue(bson: MongoType): A = - bson match + bson match { case bson: BasicDBObject => val typeName = bson.get(traitMetaData.typeDiscriminator).asInstanceOf[String] val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) formattersByTypeName(originalTypeName).fromMongoValue(bson).asInstanceOf[A] case x => throw new Exception(s"BsonObject is expected for a Trait subtype, instead got $x") - end deriveTrait - - inline private def deriveCaseClass[A]( - mirrorOfProduct: Mirror.ProductOf[A]): TypedMongoFormat[A] = - new TypedMongoFormat[A]: - private val caseClassMetaData = AnnotationReader.readCaseClassMetaData[A] - private val formatters = summonFormatters[mirrorOfProduct.MirroredElemTypes] - private val fieldsAndFormatters = caseClassMetaData.fields.zip(formatters) - - override val fieldNames: Vector[String] = fieldsAndFormatters.flatMap((field, formatter) => - if (field.embedded) formatter.fieldNames :+ field.rawName - else Vector(field.rawName)) - - override def toMongoValue(a: A): MongoType = - val bson = new BasicDBObject() - val values = a.asInstanceOf[Product].productIterator - formatters.zip(values).zip(caseClassMetaData.fields).foreach { - case ((format, value), field) => - addField(bson, field, format.toMongoValue(value)) } - bson + } - override def fromMongoValue(mongoType: MongoType): A = - mongoType match - case bson: BasicDBObject => - val fields = fieldsAndFormatters - .map { case (field, format) => - def defaultValue = field.defaultArgument.orElse(format.default) - if (field.ignored) + inline private def deriveCaseClass[A]( + mirrorOfProduct: Mirror.ProductOf[A]): TypedMongoFormat[A] = new TypedMongoFormat[A] { + private val caseClassMetaData = AnnotationReader.readCaseClassMetaData[A] + private val formatters = summonFormatters[mirrorOfProduct.MirroredElemTypes] + private val fieldsAndFormatters = caseClassMetaData.fields.zip(formatters) + + override val fieldNames: Vector[String] = fieldsAndFormatters.flatMap((field, formatter) => + if (field.embedded) formatter.fieldNames :+ field.rawName + else Vector(field.rawName)) + + override def toMongoValue(a: A): MongoType = { + val bson = new BasicDBObject() + val values = a.asInstanceOf[Product].productIterator + formatters.zip(values).zip(caseClassMetaData.fields).foreach { + case ((format, value), field) => + addField(bson, field, format.toMongoValue(value)) + } + bson + } + + override def fromMongoValue(mongoType: MongoType): A = + mongoType match { + case bson: BasicDBObject => + val fields = fieldsAndFormatters + .map { case (field, format) => + def defaultValue = field.defaultArgument.orElse(format.default) + + if (field.ignored) + defaultValue.getOrElse { + throw new Exception( + s"Missing default parameter value for ignored field `${field.name}` on deserialization.") + } + else if (field.embedded) format.fromMongoValue(bson) + else { + val value = bson.get(field.name) + if (value ne null) format.fromMongoValue(value.asInstanceOf[MongoType]) + else defaultValue.getOrElse { throw new Exception( - s"Missing default parameter value for ignored field `${field.name}` on deserialization.") + s"Missing required field '${field.name}' on deserialization.") } - else if (field.embedded) format.fromMongoValue(bson) - else { - val value = bson.get(field.name) - if (value ne null) format.fromMongoValue(value.asInstanceOf[MongoType]) - else - defaultValue.getOrElse { - throw new Exception( - s"Missing required field '${field.name}' on deserialization.") - } - } } - val tuple = Tuple.fromArray(fields.toArray) - mirrorOfProduct.fromTuple(tuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) + } + val tuple = Tuple.fromArray(fields.toArray) + mirrorOfProduct.fromTuple(tuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) - case x => throw new Exception(s"BasicDBObject is expected for a class, instead got: $x") - end deriveCaseClass + case x => throw new Exception(s"BasicDBObject is expected for a class, instead got: $x") + } + } inline private def summonFormatters[T <: Tuple]: Vector[TypedMongoFormat[Any]] = - inline erasedValue[T] match + inline erasedValue[T] match { case _: EmptyTuple => Vector.empty case _: (t *: ts) => summonInline[TypedMongoFormat[t]] .asInstanceOf[TypedMongoFormat[Any]] +: summonFormatters[ts] + } + + } +} diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala index 7ffdf4b5..9fa94df3 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala @@ -12,7 +12,7 @@ import io.sphere.mongo.generic.TypedMongoFormat.* import org.scalatest.wordspec.AnyWordSpec import org.scalatest.matchers.must.Matchers -class DerivationSpec extends AnyWordSpec with Matchers: +class DerivationSpec extends AnyWordSpec with Matchers { "MongoFormat derivation" should { "support composition" in { @@ -36,10 +36,11 @@ class DerivationSpec extends AnyWordSpec with Matchers: val format = io.sphere.mongo.generic.deriveMongoFormat[Root] - def roundtrip(member: Root): Unit = + def roundtrip(member: Root): Unit = { val bson = format.toMongoValue(member) val roundtrip = format.fromMongoValue(bson) roundtrip mustBe member + } roundtrip(Object1) roundtrip(Object2) @@ -60,3 +61,4 @@ class DerivationSpec extends AnyWordSpec with Matchers: } } +} diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala index c4895897..a587d115 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala @@ -8,7 +8,7 @@ import org.scalatest.wordspec.AnyWordSpec import java.util.UUID -object ProductTypes: +object ProductTypes { // For semi-automatic derivarion + default value argument case class Something(a: Option[Int], b: Int = 2) @@ -17,23 +17,28 @@ object ProductTypes: // Union type field - doesn't compile! // case class Identifier(idOrKey: UUID | String) derives TypedMongoFormat -end ProductTypes +} -object SumTypes: - object Color extends Enumeration: +object SumTypes { + object Color extends Enumeration { val Blue, Red, Yellow = Value + } sealed trait Coffee derives TypedMongoFormat - object Coffee: + + object Coffee { case object Espresso extends Coffee + case class Other(name: String) extends Coffee + } - enum Visitor derives TypedMongoFormat: + enum Visitor derives TypedMongoFormat { case User(email: String, password: String) case Anonymous -end SumTypes + } +} -class SerializationTest extends AnyWordSpec with Matchers: +class SerializationTest extends AnyWordSpec with Matchers { "mongoProduct" must { import ProductTypes.* @@ -179,3 +184,4 @@ class SerializationTest extends AnyWordSpec with Matchers: enumValue must be(Color.Red) } } +} diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala index 269605e1..b233f9b1 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala +++ b/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala @@ -6,7 +6,8 @@ import io.sphere.mongo.format.MongoFormat import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec -class DefaultValuesSpec extends AnyWordSpec with Matchers: +class DefaultValuesSpec extends AnyWordSpec with Matchers { + import DefaultValuesSpec.* "deriving TypedMongoFormat" must { @@ -20,13 +21,17 @@ class DefaultValuesSpec extends AnyWordSpec with Matchers: field4 mustBe Some("hi") } } +} -object DefaultValuesSpec: +object DefaultValuesSpec { private case class CaseClass( field1: String = "hello", field2: Option[String], field3: Option[String] = None, field4: Option[String] = Some("hi") ) - private object CaseClass: - implicit val mongo: TypedMongoFormat[CaseClass] = deriveMongoFormat + + private object CaseClass { + given TypedMongoFormat[CaseClass] = deriveMongoFormat + } +} From f4311e67cd3a363eff9a5570121b524d962007a2 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 19 Jul 2024 11:08:22 +0200 Subject: [PATCH 039/142] Turn off indentation based syntax. --- build.sbt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index 1c5730d2..17175662 100644 --- a/build.sbt +++ b/build.sbt @@ -44,9 +44,11 @@ lazy val standardSettings = Defaults.coreDefaultSettings ++ Seq( scalacOptions ++= Seq( "-deprecation", "-unchecked", - "-feature", - "-noindent" - ), + "-feature" + ) ++ (CrossVersion.partialVersion(scalaVersion.value) match { + case Some((3, _)) => Seq("-noindent") + case _ => Seq.empty + }), javacOptions ++= Seq("-deprecation", "-Xlint:unchecked"), // targets Java 8 bytecode (scalac & javac) ThisBuild / scalacOptions ++= { From f2cbf08ae0b3cc43a591e40eddcde574283b52cf Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Sat, 20 Jul 2024 14:19:43 +0200 Subject: [PATCH 040/142] Move sphere-json-derivation-3 to sphere-json-core (just the main, not the tests for now) --- build.sbt | 2 +- .../io/sphere/json/JSON.scala | 0 .../main/scala-3/io/sphere/json/JSON.scala | 147 +++++++++++++++++ .../json/generic/AnnotationReader.scala | 153 ++++++++++++++++++ .../sphere/json/generic/JSONAnnotation.scala | 11 ++ 5 files changed, 312 insertions(+), 1 deletion(-) rename json/json-core/src/main/{scala => scala-2}/io/sphere/json/JSON.scala (100%) create mode 100644 json/json-core/src/main/scala-3/io/sphere/json/JSON.scala create mode 100644 json/json-core/src/main/scala-3/io/sphere/json/generic/AnnotationReader.scala create mode 100644 json/json-core/src/main/scala-3/io/sphere/json/generic/JSONAnnotation.scala diff --git a/build.sbt b/build.sbt index 17175662..8e783b26 100644 --- a/build.sbt +++ b/build.sbt @@ -6,7 +6,7 @@ lazy val scala3 = "3.4.1" // sbt-github-actions needs configuration in `ThisBuild` ThisBuild / crossScalaVersions := Seq(scala2_12, scala2_13, scala3) -ThisBuild / scalaVersion := scala3 +ThisBuild / scalaVersion := scala2_13 ThisBuild / githubWorkflowPublishTargetBranches := List() ThisBuild / githubWorkflowJavaVersions := List(JavaSpec.temurin("17")) ThisBuild / githubWorkflowBuildPreamble ++= List( diff --git a/json/json-core/src/main/scala/io/sphere/json/JSON.scala b/json/json-core/src/main/scala-2/io/sphere/json/JSON.scala similarity index 100% rename from json/json-core/src/main/scala/io/sphere/json/JSON.scala rename to json/json-core/src/main/scala-2/io/sphere/json/JSON.scala diff --git a/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala new file mode 100644 index 00000000..e571ef8e --- /dev/null +++ b/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala @@ -0,0 +1,147 @@ +package io.sphere.json + +import cats.data.Validated +import cats.implicits.* +import io.sphere.json.{JSON, JSONParseError, JValidation} +import io.sphere.json.generic.{AnnotationReader, CaseClassMetaData, Field, TraitMetaData} +import org.json4s.DefaultJsonFormats.given +import org.json4s.JsonAST.JValue +import org.json4s.{DefaultJsonFormats, JObject, JString, jvalue2monadic, jvalue2readerSyntax} + +import scala.deriving.Mirror + +trait JSON[A] extends FromJSON[A] with ToJSON[A] + +inline def deriveJSON[A](using Mirror.Of[A]): JSON[A] = JSON.derived + +object JSON extends JSONInstances with JSONLowPriorityImplicits { + private val emptyFieldsSet: Vector[String] = Vector.empty + + inline def apply[A: JSON]: JSON[A] = summon[JSON[A]] + inline given derived[A](using Mirror.Of[A]): JSON[A] = Derivation.derived[A] + + private def addField(jObject: JObject, field: Field, jValue: JValue): JValue = + jValue match { + case o: JObject => + if (field.embedded) JObject(jObject.obj ++ o.obj) + else JObject(jObject.obj :+ (field.fieldName -> o)) + case other => JObject(jObject.obj :+ (field.fieldName -> other)) + } + + private object Derivation { + + import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} + + inline def derived[A](using m: Mirror.Of[A]): JSON[A] = + inline m match { + case s: Mirror.SumOf[A] => deriveTrait(s) + case p: Mirror.ProductOf[A] => deriveCaseClass(p) + } + + inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): JSON[A] = + new JSON[A] { + private val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] + private val typeHintMap: Map[String, String] = traitMetaData.subtypes.collect { + case (name, classMeta) if classMeta.typeHint.isDefined => + name -> classMeta.typeHint.get + } + private val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) + private val jsons: Seq[JSON[Any]] = summonFormatters[mirrorOfSum.MirroredElemTypes] + private val names: Seq[String] = + constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector + .asInstanceOf[Vector[String]] + private val jsonsByNames: Map[String, JSON[Any]] = names.zip(jsons).toMap + + override def read(jValue: JValue): JValidation[A] = + jValue match { + case jObject: JObject => + val typeName = (jObject \ traitMetaData.typeDiscriminator).as[String] + val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) + jsonsByNames(originalTypeName).read(jObject).map(_.asInstanceOf[A]) + case x => + Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$jValue'")) + } + + override def write(value: A): JValue = { + // we never get a trait here, only classes, it's safe to assume Product + val originalTypeName = value.asInstanceOf[Product].productPrefix + val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) + val json = jsonsByNames(originalTypeName).write(value).asInstanceOf[JObject] + val typeDiscriminator = traitMetaData.typeDiscriminator -> JString(typeName) + JObject(typeDiscriminator :: json.obj) + } + + } + + inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): JSON[A] = + new JSON[A] { + private val caseClassMetaData: CaseClassMetaData = AnnotationReader.readCaseClassMetaData[A] + private val jsons: Vector[JSON[Any]] = summonFormatters[mirrorOfProduct.MirroredElemTypes] + private val fieldsAndJsons: Vector[(Field, JSON[Any])] = caseClassMetaData.fields.zip(jsons) + + private val fieldNames: Vector[String] = fieldsAndJsons.flatMap { (field, json) => + if (field.embedded) json.fields.toVector :+ field.name + else Vector(field.name) + } + + override val fields: Set[String] = fieldNames.toSet + + override def write(value: A): JValue = { + val caseClassFields = value.asInstanceOf[Product].productIterator + jsons + .zip(caseClassFields) + .zip(caseClassMetaData.fields) + .foldLeft[JValue](JObject()) { case (jObject, ((json, fieldValue), field)) => + addField(jObject.asInstanceOf[JObject], field, json.write(fieldValue)) + } + } + + override def read(jValue: JValue): JValidation[A] = + jValue match { + case jObject: JObject => + for { + fieldsAsAList <- fieldsAndJsons + .map((field, format) => readField(field, format, jObject)) + .sequence + fieldsAsTuple = Tuple.fromArray(fieldsAsAList.toArray) + + } yield mirrorOfProduct.fromTuple( + fieldsAsTuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) + + case x => + Validated.invalidNel(JSONParseError(s"JSON object expected. $x")) + } + + private def readField(field: Field, json: JSON[Any], jObject: JObject): JValidation[Any] = + if (field.embedded) json.read(jObject) + else io.sphere.json.field(field.fieldName, field.defaultArgument)(jObject)(json) + + } + + inline private def summonFormatters[T <: Tuple]: Vector[JSON[Any]] = + inline erasedValue[T] match { + case _: EmptyTuple => Vector.empty + case _: (t *: ts) => + summonInline[JSON[t]] + .asInstanceOf[JSON[Any]] +: summonFormatters[ts] + } + } +} + +trait JSONLowPriorityImplicits { + implicit def fromJSONAndToJSON[A](implicit fromJSON: FromJSON[A], toJSON: ToJSON[A]): JSON[A] = + new JSON[A] { + override def read(jval: JValue): JValidation[A] = fromJSON.read(jval) + override def write(value: A): JValue = toJSON.write(value) + } +} + +class JSONException(msg: String) extends RuntimeException(msg) + +sealed abstract class JSONError +case class JSONFieldError(path: List[String], message: String) extends JSONError { + override def toString = path.mkString(" -> ") + ": " + message +} +case class JSONParseError(message: String) extends JSONError { + override def toString = message +} diff --git a/json/json-core/src/main/scala-3/io/sphere/json/generic/AnnotationReader.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/AnnotationReader.scala new file mode 100644 index 00000000..69c64576 --- /dev/null +++ b/json/json-core/src/main/scala-3/io/sphere/json/generic/AnnotationReader.scala @@ -0,0 +1,153 @@ +package io.sphere.json.generic + +import io.sphere.json.generic.JSONAnnotation +import io.sphere.json.generic.JSONTypeHint + +import scala.quoted.{Expr, Quotes, Type, Varargs} + +private type MA = JSONAnnotation + +case class Field( + name: String, + embedded: Boolean, + ignored: Boolean, + jsonKey: Option[JSONKey], + defaultArgument: Option[Any]) { + val fieldName: String = jsonKey.map(_.value).getOrElse(name) +} + +case class CaseClassMetaData( + name: String, + typeHintRaw: Option[JSONTypeHint], + fields: Vector[Field] +) { + val typeHint: Option[String] = + typeHintRaw.map(_.value).filterNot(_.toList.forall(_ == ' ')) +} + +case class TraitMetaData( + top: CaseClassMetaData, + typeHintFieldRaw: Option[JSONTypeHintField], + subtypes: Map[String, CaseClassMetaData] +) { + val typeDiscriminator: String = typeHintFieldRaw.map(_.value).getOrElse("type") +} + +class AnnotationReader(using q: Quotes) { + + import q.reflect.* + + def readCaseClassMetaData[T: Type]: Expr[CaseClassMetaData] = { + val sym = TypeRepr.of[T].typeSymbol + caseClassMetaData(sym) + } + + def readTraitMetaData[T: Type]: Expr[TraitMetaData] = { + val sym = TypeRepr.of[T].typeSymbol + val typeHintField = + sym.annotations.map(findJSONTypeHintField).find(_.isDefined).flatten match { + case Some(thf) => '{ Some($thf) } + case None => '{ None } + } + + '{ + TraitMetaData( + top = ${ caseClassMetaData(sym) }, + typeHintFieldRaw = $typeHintField, + subtypes = ${ subtypeAnnotations(sym) } + ) + } + } + + private def annotationTree(tree: Tree): Option[Expr[MA]] = + Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[MA]).map(_.asExprOf[MA]) + + private def findEmbedded(tree: Tree): Boolean = + Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[JSONEmbedded]).isDefined + + private def findIgnored(tree: Tree): Boolean = + Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[JSONIgnore]).isDefined + + private def findKey(tree: Tree): Option[Expr[JSONKey]] = + Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[JSONKey]).map(_.asExprOf[JSONKey]) + + private def findTypeHint(tree: Tree): Option[Expr[JSONTypeHint]] = + Option + .when(tree.isExpr)(tree.asExpr) + .filter(_.isExprOf[JSONTypeHint]) + .map(_.asExprOf[JSONTypeHint]) + + private def findJSONTypeHintField(tree: Tree): Option[Expr[JSONTypeHintField]] = + Option + .when(tree.isExpr)(tree.asExpr) + .filter(_.isExprOf[JSONTypeHintField]) + .map(_.asExprOf[JSONTypeHintField]) + + private def collectFieldInfo(companion: Symbol)(s: Symbol, paramIdx: Int): Expr[Field] = { + val embedded = Expr(s.annotations.exists(findEmbedded)) + val ignored = Expr(s.annotations.exists(findIgnored)) + val name = Expr(s.name) + val key = s.annotations.map(findKey).find(_.isDefined).flatten match { + case Some(k) => '{ Some($k) } + case None => '{ None } + } + val defArgOpt = companion + .methodMember(s"$$lessinit$$greater$$default$$${paramIdx + 1}") + .headOption + .map(dm => Ref(dm).asExprOf[Any]) match { + case Some(k) => '{ Some($k) } + case None => '{ None } + } + + '{ + Field( + name = $name, + embedded = $embedded, + ignored = $ignored, + jsonKey = $key, + defaultArgument = $defArgOpt) + } + } + + private def caseClassMetaData(sym: Symbol): Expr[CaseClassMetaData] = { + val caseParams = sym.primaryConstructor.paramSymss.take(1).flatten + val fields = Varargs(caseParams.zipWithIndex.map(collectFieldInfo(sym.companionModule))) + val name = Expr(sym.name) + val typeHint = sym.annotations.map(findTypeHint).find(_.isDefined).flatten match { + case Some(th) => '{ Some($th) } + case None => '{ None } + } + + '{ + CaseClassMetaData( + name = $name, + typeHintRaw = $typeHint, + fields = Vector($fields*) + ) + } + } + + private def subtypeAnnotation(sym: Symbol): Expr[(String, CaseClassMetaData)] = { + val name = Expr(sym.name) + val annots = caseClassMetaData(sym) + '{ ($name, $annots) } + } + + private def subtypeAnnotations(sym: Symbol): Expr[Map[String, CaseClassMetaData]] = { + val subtypes = Varargs(sym.children.map(subtypeAnnotation)) + '{ Map($subtypes*) } + } + +} + +object AnnotationReader { + inline def readCaseClassMetaData[T]: CaseClassMetaData = ${ readCaseClassMetaDataImpl[T] } + + inline def readTraitMetaData[T]: TraitMetaData = ${ readTraitMetaDataImpl[T] } + + private def readCaseClassMetaDataImpl[T: Type](using Quotes): Expr[CaseClassMetaData] = + AnnotationReader().readCaseClassMetaData[T] + + private def readTraitMetaDataImpl[T: Type](using Quotes): Expr[TraitMetaData] = + AnnotationReader().readTraitMetaData[T] +} diff --git a/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONAnnotation.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONAnnotation.scala new file mode 100644 index 00000000..7d3ace8d --- /dev/null +++ b/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONAnnotation.scala @@ -0,0 +1,11 @@ +package io.sphere.json.generic + +import scala.annotation.StaticAnnotation + +sealed trait JSONAnnotation extends StaticAnnotation + +case class JSONEmbedded() extends JSONAnnotation +case class JSONIgnore() extends JSONAnnotation +case class JSONKey(value: String) extends JSONAnnotation +case class JSONTypeHintField(value: String) extends JSONAnnotation +case class JSONTypeHint(value: String) extends JSONAnnotation From 7b84b3cf6b9f8e3f5e120cbdce448105d3f9827c Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 22 Nov 2024 18:05:04 +0100 Subject: [PATCH 041/142] move mongo-derivation-3 to mongo-3, separating the MongoFormat implementation from mongo-core making it a fully standalone scala3 module --- build.sbt | 12 +- mongo/mongo-3/dependencies.sbt | 3 + .../mongo/catsinstances/catsinstances.scala | 22 +++ .../main/scala/io/sphere/mongo/format.scala | 8 + .../io/sphere/mongo/format/MongoFormat.scala | 152 ++++++++++++++++++ .../mongo/generic/AnnotationReader.scala | 7 +- .../io/sphere/mongo/generic/Annotations.scala | 0 .../mongo/generic/DefaultMongoFormats.scala | 39 +++++ .../src/test/scala/MongoUtils.scala | 0 .../io/sphere/mongo/DerivationSpec.scala | 8 +- .../io/sphere/mongo/SerializationTest.scala | 37 ++--- .../mongo/format/OptionMongoFormatSpec.scala | 18 +-- .../mongo/generic/DefaultValuesSpec.scala | 10 +- .../mongo/generic/DeriveMongoFormatSpec.scala | 13 +- .../mongo/generic/MongoEmbeddedSpec.scala | 1 + .../mongo/generic/MongoIgnoreSpec.scala | 1 + .../sphere/mongo/generic/MongoKeySpec.scala | 1 + ...goTypeHintFieldWithAbstractClassSpec.scala | 4 +- ...ongoTypeHintFieldWithSealedTraitSpec.scala | 2 +- .../mongo/generic/SumTypesDerivingSpec.scala | 29 ++-- .../main/scala/io/sphere/mongo/format.scala | 10 -- .../mongo/generic/DefaultMongoFormats.scala | 38 ----- .../io/sphere/mongo/generic/Derivation.scala | 152 ------------------ 23 files changed, 298 insertions(+), 269 deletions(-) create mode 100644 mongo/mongo-3/dependencies.sbt create mode 100644 mongo/mongo-3/src/main/scala/io/sphere/mongo/catsinstances/catsinstances.scala create mode 100644 mongo/mongo-3/src/main/scala/io/sphere/mongo/format.scala create mode 100644 mongo/mongo-3/src/main/scala/io/sphere/mongo/format/MongoFormat.scala rename mongo/{mongo-derivation-scala-3 => mongo-3}/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala (94%) rename mongo/{mongo-derivation-scala-3 => mongo-3}/src/main/scala/io/sphere/mongo/generic/Annotations.scala (100%) create mode 100644 mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/DefaultMongoFormats.scala rename mongo/{mongo-derivation-scala-3 => mongo-3}/src/test/scala/MongoUtils.scala (100%) rename mongo/{mongo-derivation-scala-3 => mongo-3}/src/test/scala/io/sphere/mongo/DerivationSpec.scala (86%) rename mongo/{mongo-derivation-scala-3 => mongo-3}/src/test/scala/io/sphere/mongo/SerializationTest.scala (81%) rename mongo/{mongo-derivation-scala-3 => mongo-3}/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala (76%) rename mongo/{mongo-derivation-scala-3 => mongo-3}/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala (75%) rename mongo/{mongo-derivation-scala-3 => mongo-3}/src/test/scala/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala (88%) rename mongo/{mongo-derivation-scala-3 => mongo-3}/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala (98%) rename mongo/{mongo-derivation-scala-3 => mongo-3}/src/test/scala/io/sphere/mongo/generic/MongoIgnoreSpec.scala (95%) rename mongo/{mongo-derivation-scala-3 => mongo-3}/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala (96%) rename mongo/{mongo-derivation-scala-3 => mongo-3}/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala (91%) rename mongo/{mongo-derivation-scala-3 => mongo-3}/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala (96%) rename mongo/{mongo-derivation-scala-3 => mongo-3}/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala (88%) delete mode 100644 mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/format.scala delete mode 100644 mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/DefaultMongoFormats.scala delete mode 100644 mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala diff --git a/build.sbt b/build.sbt index 2a5d015e..fffa2165 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ import pl.project13.scala.sbt.JmhPlugin lazy val scala2_12 = "2.12.19" lazy val scala2_13 = "2.13.14" -lazy val scala3 = "3.4.1" +lazy val scala3 = "3.5.2" // sbt-github-actions needs configuration in `ThisBuild` ThisBuild / crossScalaVersions := Seq(scala2_12, scala2_13, scala3) @@ -105,7 +105,7 @@ lazy val `sphere-json-derivation` = project .dependsOn(`sphere-json-core`) lazy val `sphere-json-derivation-scala-3` = project - .settings(crossScalaVersions := Seq(scala3)) + .settings(scalaVersion := scala3) .in(file("./json/json-derivation-scala-3")) .settings(standardSettings: _*) .dependsOn(`sphere-json-core`) @@ -129,11 +129,11 @@ lazy val `sphere-mongo-derivation` = project .settings(Fmpp.settings: _*) .dependsOn(`sphere-mongo-core`) -lazy val `sphere-mongo-derivation-scala-3` = project - .settings(crossScalaVersions := Seq(scala3)) - .in(file("./mongo/mongo-derivation-scala-3")) +lazy val `sphere-mongo-3` = project + .settings(scalaVersion := scala3) + .in(file("./mongo/mongo-3")) .settings(standardSettings: _*) - .dependsOn(`sphere-mongo-core`) + .dependsOn(`sphere-util`) lazy val `sphere-mongo-derivation-magnolia` = project .in(file("./mongo/mongo-derivation-magnolia")) diff --git a/mongo/mongo-3/dependencies.sbt b/mongo/mongo-3/dependencies.sbt new file mode 100644 index 00000000..a25186f0 --- /dev/null +++ b/mongo/mongo-3/dependencies.sbt @@ -0,0 +1,3 @@ +libraryDependencies ++= Seq( + "org.mongodb" % "mongodb-driver-core" % "5.1.2" // tracking http://mongodb.github.io/mongo-java-driver/ +) diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/catsinstances/catsinstances.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/catsinstances/catsinstances.scala new file mode 100644 index 00000000..1b6d9d4f --- /dev/null +++ b/mongo/mongo-3/src/main/scala/io/sphere/mongo/catsinstances/catsinstances.scala @@ -0,0 +1,22 @@ +package io.sphere.mongo + +import _root_.cats.Invariant +import io.sphere.mongo.format.MongoFormat + +/** Cats instances for [[MongoFormat]] + */ +package object catsinstances extends MongoFormatInstances + +trait MongoFormatInstances { + implicit val catsInvariantForMongoFormat: Invariant[MongoFormat] = + new MongoFormatInvariant +} + +class MongoFormatInvariant extends Invariant[MongoFormat] { + override def imap[A, B](fa: MongoFormat[A])(f: A => B)(g: B => A): MongoFormat[B] = + new MongoFormat[B] { + override def toMongoValue(b: B): Any = fa.toMongoValue(g(b)) + override def fromMongoValue(any: Any): B = f(fa.fromMongoValue(any)) + override val fieldNames: Vector[String] = fa.fieldNames + } +} diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/format.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/format.scala new file mode 100644 index 00000000..3483dc12 --- /dev/null +++ b/mongo/mongo-3/src/main/scala/io/sphere/mongo/format.scala @@ -0,0 +1,8 @@ +package io.sphere.mongo + +import io.sphere.mongo.generic +import io.sphere.mongo.format.MongoFormat + +def toMongo[A: MongoFormat](a: A): Any = summon[MongoFormat[A]].toMongoValue(a) + +def fromMongo[A: MongoFormat](any: Any): A = summon[MongoFormat[A]].fromMongoValue(any) diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/MongoFormat.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/MongoFormat.scala new file mode 100644 index 00000000..ffcf9856 --- /dev/null +++ b/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/MongoFormat.scala @@ -0,0 +1,152 @@ +package io.sphere.mongo.format + +import com.mongodb.BasicDBObject +import io.sphere.mongo.generic.{AnnotationReader, Field} +import org.bson.types.ObjectId + +import java.util.UUID +import java.util.regex.Pattern +import scala.deriving.Mirror + +object MongoNothing + +type SimpleMongoType = UUID | String | ObjectId | Short | Int | Long | Float | Double | Boolean | + Pattern + +trait MongoFormat[A] extends Serializable { + def toMongoValue(a: A): Any + def fromMongoValue(mongoType: Any): A + + // /** needed JSON fields - ignored if empty */ + val fieldNames: Vector[String] = MongoFormat.emptyFields + + def default: Option[A] = None +} +final class NativeMongoFormat[A] extends MongoFormat[A] { + def toMongoValue(a: A): Any = a + def fromMongoValue(any: Any): A = any.asInstanceOf[A] +} + +inline def deriveMongoFormat[A](using Mirror.Of[A]): MongoFormat[A] = MongoFormat.derived + +object MongoFormat { + inline def apply[A: MongoFormat]: MongoFormat[A] = summon + + private val emptyFields: Vector[String] = Vector.empty + + inline given derived[A](using Mirror.Of[A]): MongoFormat[A] = Derivation.derived + + private def addField(bson: BasicDBObject, field: Field, mongoType: Any): Unit = + if (!field.ignored) + mongoType match { + case s: SimpleMongoType => bson.put(field.name, s) + case innerBson: BasicDBObject => + if (field.embedded) innerBson.entrySet().forEach(p => bson.put(p.getKey, p.getValue)) + else bson.put(field.name, innerBson) + case MongoNothing => + } + + private object Derivation { + + import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} + + inline def derived[A](using m: Mirror.Of[A]): MongoFormat[A] = + inline m match { + case s: Mirror.SumOf[A] => deriveTrait(s) + case p: Mirror.ProductOf[A] => deriveCaseClass(p) + } + + inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): MongoFormat[A] = + new MongoFormat[A] { + private val traitMetaData = AnnotationReader.readTraitMetaData[A] + private val typeHintMap = traitMetaData.subtypes.collect { + case (name, classMeta) if classMeta.typeHint.isDefined => + name -> classMeta.typeHint.get + } + private val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) + private val formatters = summonFormatters[mirrorOfSum.MirroredElemTypes] + private val names = constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector + .asInstanceOf[Vector[String]] + private val formattersByTypeName = names.zip(formatters).toMap + + override def toMongoValue(a: A): Any = { + // we never get a trait here, only classes, it's safe to assume Product + val originalTypeName = a.asInstanceOf[Product].productPrefix + val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) + val bson = + formattersByTypeName(originalTypeName).toMongoValue(a).asInstanceOf[BasicDBObject] + bson.put(traitMetaData.typeDiscriminator, typeName) + bson + } + + override def fromMongoValue(bson: Any): A = + bson match { + case bson: BasicDBObject => + val typeName = bson.get(traitMetaData.typeDiscriminator).asInstanceOf[String] + val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) + formattersByTypeName(originalTypeName).fromMongoValue(bson).asInstanceOf[A] + case x => + throw new Exception(s"BsonObject is expected for a Trait subtype, instead got $x") + } + } + + inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): MongoFormat[A] = + new MongoFormat[A] { + private val caseClassMetaData = AnnotationReader.readCaseClassMetaData[A] + private val formatters = summonFormatters[mirrorOfProduct.MirroredElemTypes] + private val fieldsAndFormatters = caseClassMetaData.fields.zip(formatters) + + override val fieldNames: Vector[String] = fieldsAndFormatters.flatMap((field, formatter) => + if (field.embedded) formatter.fieldNames :+ field.rawName + else Vector(field.rawName)) + + override def toMongoValue(a: A): Any = { + val bson = new BasicDBObject() + val values = a.asInstanceOf[Product].productIterator + formatters.zip(values).zip(caseClassMetaData.fields).foreach { + case ((format, value), field) => + addField(bson, field, format.toMongoValue(value)) + } + bson + } + + override def fromMongoValue(mongoType: Any): A = + mongoType match { + case bson: BasicDBObject => + val fields = fieldsAndFormatters + .map { case (field, format) => + def defaultValue = field.defaultArgument.orElse(format.default) + + if (field.ignored) + defaultValue.getOrElse { + throw new Exception( + s"Missing default parameter value for ignored field `${field.name}` on deserialization.") + } + else if (field.embedded) format.fromMongoValue(bson) + else { + val value = bson.get(field.name) + if (value ne null) format.fromMongoValue(value.asInstanceOf[Any]) + else + defaultValue.getOrElse { + throw new Exception( + s"Missing required field '${field.name}' on deserialization.") + } + } + } + val tuple = Tuple.fromArray(fields.toArray) + mirrorOfProduct.fromTuple(tuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) + + case x => throw new Exception(s"BasicDBObject is expected for a class, instead got: $x") + } + } + + inline private def summonFormatters[T <: Tuple]: Vector[MongoFormat[Any]] = + inline erasedValue[T] match { + case _: EmptyTuple => Vector.empty + case _: (t *: ts) => + summonInline[MongoFormat[t]] + .asInstanceOf[MongoFormat[Any]] +: summonFormatters[ts] + } + + } +} diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala similarity index 94% rename from mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala rename to mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala index 7ac57f36..7b5c5a91 100644 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala +++ b/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala @@ -1,6 +1,7 @@ package io.sphere.mongo.generic import scala.quoted.{Expr, Quotes, Type, Varargs} +import io.sphere.mongo.format.MongoFormat private type MA = MongoAnnotation @@ -31,10 +32,10 @@ case class TraitMetaData( object AnnotationReader { - def mongoEnum(e: Enumeration): TypedMongoFormat[e.Value] = new TypedMongoFormat[e.Value] { - def toMongoValue(a: e.Value): MongoType = a.toString + def mongoEnum(e: Enumeration): MongoFormat[e.Value] = new MongoFormat[e.Value] { + def toMongoValue(a: e.Value): Any = a.toString - def fromMongoValue(any: MongoType): e.Value = e.withName(any.asInstanceOf[String]) + def fromMongoValue(any: Any): e.Value = e.withName(any.asInstanceOf[String]) } inline def readTraitMetaData[T]: TraitMetaData = ${ readTraitMetaDataImpl[T] } diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Annotations.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/Annotations.scala similarity index 100% rename from mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Annotations.scala rename to mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/Annotations.scala diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/DefaultMongoFormats.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/DefaultMongoFormats.scala new file mode 100644 index 00000000..0ce5de71 --- /dev/null +++ b/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/DefaultMongoFormats.scala @@ -0,0 +1,39 @@ +package io.sphere.mongo.generic + +import com.mongodb.BasicDBObject +import io.sphere.mongo.format.{MongoFormat, MongoNothing, NativeMongoFormat, SimpleMongoType} + +object DefaultMongoFormats extends DefaultMongoFormats {} + +trait DefaultMongoFormats { + given MongoFormat[Int] = new NativeMongoFormat[Int] + + given MongoFormat[String] = new NativeMongoFormat[String] + + given MongoFormat[Boolean] = new NativeMongoFormat[Boolean] + + given [A](using MongoFormat[A]): MongoFormat[Option[A]] = + new MongoFormat[Option[A]] { + override def toMongoValue(a: Option[A]): Any = + a match { + case Some(value) => summon[MongoFormat[A]].toMongoValue(value) + case None => MongoNothing + } + + override def fromMongoValue(mongoType: Any): Option[A] = { + val fieldNames = summon[MongoFormat[A]].fieldNames + if (mongoType == null) None + else + mongoType match { + case s: SimpleMongoType => Some(summon[MongoFormat[A]].fromMongoValue(s)) + case bson: BasicDBObject => + val bsonFieldNames = bson.keySet().toArray + if (fieldNames.nonEmpty && bsonFieldNames.intersect(fieldNames).isEmpty) None + else Some(summon[MongoFormat[A]].fromMongoValue(bson)) + case MongoNothing => None // This can't happen, but it makes the compiler happy + } + } + + override def default: Option[Option[A]] = Some(None) + } +} diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/MongoUtils.scala b/mongo/mongo-3/src/test/scala/MongoUtils.scala similarity index 100% rename from mongo/mongo-derivation-scala-3/src/test/scala/MongoUtils.scala rename to mongo/mongo-3/src/test/scala/MongoUtils.scala diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala similarity index 86% rename from mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala rename to mongo/mongo-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala index 9fa94df3..f7e074be 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala @@ -4,11 +4,9 @@ import io.sphere.mongo.generic.{ AnnotationReader, MongoEmbedded, MongoKey, - MongoTypeHintField, - TypedMongoFormat + MongoTypeHintField } import io.sphere.mongo.generic.DefaultMongoFormats.given -import io.sphere.mongo.generic.TypedMongoFormat.* import org.scalatest.wordspec.AnyWordSpec import org.scalatest.matchers.must.Matchers @@ -19,7 +17,7 @@ class DerivationSpec extends AnyWordSpec with Matchers { case class Container(i: Int, str: String, component: Component) case class Component(i: Int) - val format = io.sphere.mongo.generic.deriveMongoFormat[Container] + val format = io.sphere.mongo.format.deriveMongoFormat[Container] val container = Container(123, "anything", Component(456)) val bson = format.toMongoValue(container) @@ -34,7 +32,7 @@ class DerivationSpec extends AnyWordSpec with Matchers { case object Object2 extends Root case class Class(i: Int) extends Root - val format = io.sphere.mongo.generic.deriveMongoFormat[Root] + val format = io.sphere.mongo.format.deriveMongoFormat[Root] def roundtrip(member: Root): Unit = { val bson = format.toMongoValue(member) diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/SerializationTest.scala similarity index 81% rename from mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala rename to mongo/mongo-3/src/test/scala/io/sphere/mongo/SerializationTest.scala index a587d115..9d221ca4 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/SerializationTest.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/SerializationTest.scala @@ -1,7 +1,8 @@ package io.sphere.mongo import com.mongodb.BasicDBObject -import io.sphere.mongo.generic.{AnnotationReader, DefaultMongoFormats, TypedMongoFormat} +import io.sphere.mongo.format.MongoFormat +import io.sphere.mongo.generic.{AnnotationReader, DefaultMongoFormats} import io.sphere.mongo.generic.DefaultMongoFormats.given import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -13,7 +14,7 @@ object ProductTypes { case class Something(a: Option[Int], b: Int = 2) // For Automatic derivation with `derives` - case class Frunfles(a: Option[Int], b: Int) derives TypedMongoFormat + case class Frunfles(a: Option[Int], b: Int) derives MongoFormat // Union type field - doesn't compile! // case class Identifier(idOrKey: UUID | String) derives TypedMongoFormat @@ -24,7 +25,7 @@ object SumTypes { val Blue, Red, Yellow = Value } - sealed trait Coffee derives TypedMongoFormat + sealed trait Coffee derives MongoFormat object Coffee { case object Espresso extends Coffee @@ -32,7 +33,7 @@ object SumTypes { case class Other(name: String) extends Coffee } - enum Visitor derives TypedMongoFormat { + enum Visitor derives MongoFormat { case User(email: String, password: String) case Anonymous } @@ -48,23 +49,23 @@ class SerializationTest extends AnyWordSpec with Matchers { dbo.put("b", Integer.valueOf(4)) // Using backwards-compatible `deriveMongoFormat` + `implicit` - implicit val x: TypedMongoFormat[Something] = io.sphere.mongo.generic.deriveMongoFormat + implicit val x: MongoFormat[Something] = io.sphere.mongo.format.deriveMongoFormat - val something = TypedMongoFormat[Something].fromMongoValue(dbo) + val something = MongoFormat[Something].fromMongoValue(dbo) something mustBe Something(Some(3), 4) } "generate a format that serializes optional fields with value None as BSON objects without that field" in { // Using new Scala 3 `derived` special method + `given` - given TypedMongoFormat[Something] = TypedMongoFormat.derived + given MongoFormat[Something] = MongoFormat.derived val something = Something(None, 1) val serializedObject = - TypedMongoFormat[Something].toMongoValue(something).asInstanceOf[BasicDBObject] + MongoFormat[Something].toMongoValue(something).asInstanceOf[BasicDBObject] serializedObject.keySet().contains("b") must be(true) serializedObject.keySet().contains("a") must be(false) - TypedMongoFormat[Something].fromMongoValue(serializedObject) must be(something) + MongoFormat[Something].fromMongoValue(serializedObject) must be(something) } "generate a format that serializes optional fields with value None as BSON objects without that field (using derives)" in { @@ -72,11 +73,11 @@ class SerializationTest extends AnyWordSpec with Matchers { val frunfles = Frunfles(None, 1) val serializedObject = - TypedMongoFormat[Frunfles].toMongoValue(frunfles).asInstanceOf[BasicDBObject] + MongoFormat[Frunfles].toMongoValue(frunfles).asInstanceOf[BasicDBObject] serializedObject.keySet().contains("b") must be(true) serializedObject.keySet().contains("a") must be(false) - TypedMongoFormat[Frunfles].fromMongoValue(serializedObject) must be(frunfles) + MongoFormat[Frunfles].fromMongoValue(serializedObject) must be(frunfles) } // https://stackoverflow.com/questions/68421043/type-class-derivation-accessing-default-values @@ -86,11 +87,11 @@ class SerializationTest extends AnyWordSpec with Matchers { dbo.put("a", Integer.valueOf(3)) dbo } - val s1 = TypedMongoFormat[Something].fromMongoValue(sthObj1) + val s1 = MongoFormat[Something].fromMongoValue(sthObj1) s1 must be(Something(a = Some(3), b = 2)) val sthObj2 = new BasicDBObject() // an empty object - val s2 = TypedMongoFormat[Something].fromMongoValue(sthObj2) + val s2 = MongoFormat[Something].fromMongoValue(sthObj2) s2 must be(Something(a = None, b = 2)) val sthObj3 = { @@ -98,7 +99,7 @@ class SerializationTest extends AnyWordSpec with Matchers { dbo.put("b", Integer.valueOf(33)) dbo } - val s3 = TypedMongoFormat[Something].fromMongoValue(sthObj3) + val s3 = MongoFormat[Something].fromMongoValue(sthObj3) s3 must be(Something(a = None, b = 33)) val sthObj4 = { @@ -107,7 +108,7 @@ class SerializationTest extends AnyWordSpec with Matchers { dbo.put("b", Integer.valueOf(44)) dbo } - val s4 = TypedMongoFormat[Something].fromMongoValue(sthObj4) + val s4 = MongoFormat[Something].fromMongoValue(sthObj4) s4 must be(Something(a = Some(33), b = 44)) } } @@ -118,7 +119,7 @@ class SerializationTest extends AnyWordSpec with Matchers { import SumTypes.* "serialize and deserialize sealed hierarchies" in { - val mongo = TypedMongoFormat[Coffee] + val mongo = MongoFormat[Coffee] val espressoObj = { val dbo = new BasicDBObject @@ -145,7 +146,7 @@ class SerializationTest extends AnyWordSpec with Matchers { } "serialize and deserialize enums" in { - val mongo = TypedMongoFormat[Visitor] + val mongo = MongoFormat[Visitor] val anonObj = { val dbo = new BasicDBObject @@ -174,7 +175,7 @@ class SerializationTest extends AnyWordSpec with Matchers { } "serialize and deserialize enumerations" in { - val mongo: TypedMongoFormat[Color.Value] = AnnotationReader.mongoEnum(Color) + val mongo: MongoFormat[Color.Value] = AnnotationReader.mongoEnum(Color) // mongo java driver knows how to encode/decode Strings val serializedObject = mongo.toMongoValue(Color.Red).asInstanceOf[String] diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala similarity index 76% rename from mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala rename to mongo/mongo-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala index 2e4688d0..40e99c4f 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala @@ -12,13 +12,13 @@ object OptionMongoFormatSpec { case class SimpleClass(value1: String, value2: Int) object SimpleClass { - given TypedMongoFormat[SimpleClass] = deriveMongoFormat[SimpleClass] + given MongoFormat[SimpleClass] = deriveMongoFormat[SimpleClass] } case class ComplexClass(name: String, simpleClass: Option[SimpleClass]) object ComplexClass { - given TypedMongoFormat[ComplexClass] = deriveMongoFormat[ComplexClass] + given MongoFormat[ComplexClass] = deriveMongoFormat[ComplexClass] } } @@ -32,7 +32,7 @@ class OptionMongoFormatSpec extends AnyWordSpec with Matchers with OptionValues "value1" -> "a", "value2" -> 45 ) - val result = TypedMongoFormat[Option[SimpleClass]].fromMongoValue(dbo) + val result = MongoFormat[Option[SimpleClass]].fromMongoValue(dbo) result.value.value1 mustEqual "a" result.value.value2 mustEqual 45 } @@ -43,25 +43,25 @@ class OptionMongoFormatSpec extends AnyWordSpec with Matchers with OptionValues "value2" -> 45, "value3" -> "b" ) - val result = TypedMongoFormat[Option[SimpleClass]].fromMongoValue(dbo) + val result = MongoFormat[Option[SimpleClass]].fromMongoValue(dbo) result.value.value1 mustEqual "a" result.value.value2 mustEqual 45 } "handle presence of not all the fields" in { val dbo = dbObj("value1" -> "a") - an[Exception] mustBe thrownBy(TypedMongoFormat[Option[SimpleClass]].fromMongoValue(dbo)) + an[Exception] mustBe thrownBy(MongoFormat[Option[SimpleClass]].fromMongoValue(dbo)) } "handle absence of all fields" in { val dbo = dbObj() - val result = TypedMongoFormat[Option[SimpleClass]].fromMongoValue(dbo) + val result = MongoFormat[Option[SimpleClass]].fromMongoValue(dbo) result mustEqual None } "handle absence of all fields mixed with ignored fields" in { val dbo = dbObj("value3" -> "a") - val result = TypedMongoFormat[Option[SimpleClass]].fromMongoValue(dbo) + val result = MongoFormat[Option[SimpleClass]].fromMongoValue(dbo) result mustEqual None } @@ -86,11 +86,11 @@ class OptionMongoFormatSpec extends AnyWordSpec with Matchers with OptionValues "value2" -> 42 ) ) - val result = TypedMongoFormat[ComplexClass].fromMongoValue(dbo) + val result = MongoFormat[ComplexClass].fromMongoValue(dbo) result.simpleClass.value.value1 mustEqual "value1" result.simpleClass.value.value2 mustEqual 42 - TypedMongoFormat[ComplexClass].toMongoValue(result) mustEqual dbo + MongoFormat[ComplexClass].toMongoValue(result) mustEqual dbo } } } diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala similarity index 75% rename from mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala rename to mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala index b233f9b1..b5b4cd21 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala @@ -2,7 +2,7 @@ package io.sphere.mongo.generic import io.sphere.mongo.MongoUtils.* import io.sphere.mongo.generic.DefaultMongoFormats.given -import io.sphere.mongo.format.MongoFormat +import io.sphere.mongo.format.{MongoFormat, deriveMongoFormat} import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -13,7 +13,7 @@ class DefaultValuesSpec extends AnyWordSpec with Matchers { "deriving TypedMongoFormat" must { "handle default values" in { val dbo = dbObj() - val test = TypedMongoFormat[CaseClass].fromMongoValue(dbo) + val test = MongoFormat[CaseClass].fromMongoValue(dbo) import test._ field1 mustBe "hello" field2 mustBe None @@ -24,14 +24,14 @@ class DefaultValuesSpec extends AnyWordSpec with Matchers { } object DefaultValuesSpec { - private case class CaseClass( + case class CaseClass( field1: String = "hello", field2: Option[String], field3: Option[String] = None, field4: Option[String] = Some("hi") ) - private object CaseClass { - given TypedMongoFormat[CaseClass] = deriveMongoFormat + object CaseClass { + given MongoFormat[CaseClass] = deriveMongoFormat[CaseClass] } } diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala similarity index 88% rename from mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala rename to mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala index 6accfd78..8ac56f87 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala @@ -4,6 +4,7 @@ import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec import io.sphere.mongo.generic.DefaultMongoFormats.given import io.sphere.mongo.MongoUtils.* +import io.sphere.mongo.format.{MongoFormat, deriveMongoFormat} import io.sphere.mongo.{fromMongo, toMongo} class DeriveMongoFormatSpec extends AnyWordSpec with Matchers { @@ -89,11 +90,11 @@ class DeriveMongoFormatSpec extends AnyWordSpec with Matchers { "fail to derive if trait is not sealed" in { // Sealed - "implicit val mongo: TypedMongoFormat[SealedSub] = deriveMongoFormat[SealedSub]" must compile + "implicit val mongo: MongoFormat[SealedSub] = deriveMongoFormat[SealedSub]" must compile // Not sealed - "implicit val mongo: TypedMongoFormat[NotSealed] = deriveMongoFormat[NotSealed]" mustNot compile + "implicit val mongo: MongoFormat[NotSealed] = deriveMongoFormat[NotSealed]" mustNot compile // Sealed, but child is not sealed - "implicit val mongo: TypedMongoFormat[SealedParent] = deriveMongoFormat[SealedParent]" mustNot compile + "implicit val mongo: MongoFormat[SealedParent] = deriveMongoFormat[SealedParent]" mustNot compile } } } @@ -106,7 +107,7 @@ object DeriveMongoFormatSpec { @MongoTypeHint(value = "bar") case class Custom(width: Int, height: Int) extends PictureSize - given TypedMongoFormat[PictureSize] = deriveMongoFormat[PictureSize] + given MongoFormat[PictureSize] = deriveMongoFormat[PictureSize] sealed trait Access object Access { @@ -114,7 +115,7 @@ object DeriveMongoFormatSpec { case class Authorized(project: String) extends Access } - given TypedMongoFormat[Access] = deriveMongoFormat + given MongoFormat[Access] = deriveMongoFormat case class UserWithPicture( userId: String, @@ -122,7 +123,7 @@ object DeriveMongoFormatSpec { pictureUrl: String, access: Option[Access] = None) - given TypedMongoFormat[UserWithPicture] = deriveMongoFormat + given MongoFormat[UserWithPicture] = deriveMongoFormat sealed trait SealedParent diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala similarity index 98% rename from mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala rename to mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala index 9c1b1779..741152bd 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala @@ -1,6 +1,7 @@ package io.sphere.mongo.generic import io.sphere.mongo.MongoUtils.* +import io.sphere.mongo.format.deriveMongoFormat import io.sphere.mongo.generic.DefaultMongoFormats.given import org.scalatest.OptionValues import org.scalatest.matchers.must.Matchers diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoIgnoreSpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoIgnoreSpec.scala similarity index 95% rename from mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoIgnoreSpec.scala rename to mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoIgnoreSpec.scala index d88b2167..dcda4d6f 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoIgnoreSpec.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoIgnoreSpec.scala @@ -1,6 +1,7 @@ package io.sphere.mongo.generic import io.sphere.mongo.MongoUtils.* +import io.sphere.mongo.format.deriveMongoFormat import io.sphere.mongo.generic.DefaultMongoFormats.given import org.scalatest.OptionValues import org.scalatest.matchers.must.Matchers diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala similarity index 96% rename from mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala rename to mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala index 47e223db..d022a1f0 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala @@ -1,6 +1,7 @@ package io.sphere.mongo.generic import com.mongodb.BasicDBObject +import io.sphere.mongo.format.deriveMongoFormat import io.sphere.mongo.generic.DefaultMongoFormats.given import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala similarity index 91% rename from mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala rename to mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala index c9de53f1..b820a7aa 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala @@ -1,7 +1,7 @@ package io.sphere.mongo.generic import io.sphere.mongo.MongoUtils.dbObj -import io.sphere.mongo.format.{MongoFormat, fromMongo, toMongo} +import io.sphere.mongo.format.{MongoFormat, deriveMongoFormat} import io.sphere.mongo.generic.DefaultMongoFormats.given import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -48,6 +48,6 @@ object MongoTypeHintFieldWithAbstractClassSpec { case class UserWithPicture(userId: String, pictureSize: PictureSize, pictureUrl: String) object UserWithPicture { - val mongo: TypedMongoFormat[UserWithPicture] = deriveMongoFormat[UserWithPicture] + val mongo: MongoFormat[UserWithPicture] = deriveMongoFormat[UserWithPicture] } } diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala similarity index 96% rename from mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala rename to mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala index a14c693b..9592f1cd 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala @@ -1,7 +1,7 @@ package io.sphere.mongo.generic import io.sphere.mongo.MongoUtils.dbObj -import io.sphere.mongo.format.{MongoFormat, fromMongo, toMongo} +import io.sphere.mongo.format.{MongoFormat, deriveMongoFormat} import io.sphere.mongo.generic.DefaultMongoFormats.given import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec diff --git a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala similarity index 88% rename from mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala rename to mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala index b639260a..dd31c9a8 100644 --- a/mongo/mongo-derivation-scala-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala @@ -2,6 +2,7 @@ package io.sphere.mongo.generic import com.mongodb.DBObject import io.sphere.mongo.MongoUtils.dbObj +import io.sphere.mongo.format.{MongoFormat, deriveMongoFormat} import io.sphere.mongo.generic.DefaultMongoFormats.given import org.bson.BSONObject import org.scalatest.Assertion @@ -113,7 +114,7 @@ class SumTypesDerivingSpec extends AnyWordSpec with Matchers { object SumTypesDerivingSpec { import Matchers.* - def check[A, B <: A](format: TypedMongoFormat[A], b: B, dbo: DBObject): Assertion = { + def check[A, B <: A](format: MongoFormat[A], b: B, dbo: DBObject): Assertion = { val serialized = format.toMongoValue(b) serialized must be(dbo) @@ -210,10 +211,10 @@ object SumTypesDerivingSpec { case object Red extends Color10 case class Custom(rgb: String) extends Color10 - implicit val redFormatter: TypedMongoFormat[Red.type] = new TypedMongoFormat[Red.type] { - override def toMongoValue(a: Red.type): MongoType = + implicit val redFormatter: MongoFormat[Red.type] = new MongoFormat[Red.type] { + override def toMongoValue(a: Red.type): Any = dbObj("type" -> "Red", "extraField" -> "panda") - override def fromMongoValue(any: MongoType): Red.type = Red + override def fromMongoValue(any: Any): Red.type = Red } val format = deriveMongoFormat[Color10] } @@ -223,10 +224,10 @@ object SumTypesDerivingSpec { case object Red extends Color11 case class Custom(rgb: String) extends Color11 - implicit val customFormatter: TypedMongoFormat[Custom] = new TypedMongoFormat[Custom] { - override def toMongoValue(a: Custom): MongoType = + implicit val customFormatter: MongoFormat[Custom] = new MongoFormat[Custom] { + override def toMongoValue(a: Custom): Any = dbObj("type" -> "Custom", "rgb" -> a.rgb, "extraField" -> "panda") - override def fromMongoValue(any: MongoType): Custom = + override def fromMongoValue(any: Any): Custom = Custom(any.asInstanceOf[BSONObject].get("rgb").asInstanceOf[String]) } val format = deriveMongoFormat[Color11] @@ -242,11 +243,11 @@ object SumTypesDerivingSpec { case object Red extends ColorUpperBound case class Custom[Type1 <: Bound](rgb: String) extends ColorUpperBound - implicit def customFormatter[A <: Bound]: TypedMongoFormat[Custom[A]] = - new TypedMongoFormat[Custom[A]] { - override def toMongoValue(a: Custom[A]): MongoType = + implicit def customFormatter[A <: Bound]: MongoFormat[Custom[A]] = + new MongoFormat[Custom[A]] { + override def toMongoValue(a: Custom[A]): Any = dbObj("type" -> "Custom", "rgb" -> a.rgb, "extraField" -> "panda") - override def fromMongoValue(any: MongoType): Custom[A] = + override def fromMongoValue(any: Any): Custom[A] = Custom(any.asInstanceOf[BSONObject].get("rgb").asInstanceOf[String]) } @@ -258,10 +259,10 @@ object SumTypesDerivingSpec { case object Red extends ColorUnbound case class Custom[A](rgb: String) extends ColorUnbound - implicit def customFormatter[A]: TypedMongoFormat[Custom[A]] = new TypedMongoFormat[Custom[A]] { - override def toMongoValue(a: Custom[A]): MongoType = + implicit def customFormatter[A]: MongoFormat[Custom[A]] = new MongoFormat[Custom[A]] { + override def toMongoValue(a: Custom[A]): Any = dbObj("type" -> "Custom", "rgb" -> a.rgb, "extraField" -> "panda") - override def fromMongoValue(any: MongoType): Custom[A] = + override def fromMongoValue(any: Any): Custom[A] = Custom(any.asInstanceOf[BSONObject].get("rgb").asInstanceOf[String]) } diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/format.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/format.scala deleted file mode 100644 index a16d7771..00000000 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/format.scala +++ /dev/null @@ -1,10 +0,0 @@ -package io.sphere.mongo - -import io.sphere.mongo.generic -import io.sphere.mongo.generic.{MongoType, TypedMongoFormat} - -def toMongo[A: TypedMongoFormat](a: A): MongoType = - summon[generic.TypedMongoFormat[A]].toMongoValue(a) - -def fromMongo[A: TypedMongoFormat](any: MongoType): A = - summon[generic.TypedMongoFormat[A]].fromMongoValue(any) diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/DefaultMongoFormats.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/DefaultMongoFormats.scala deleted file mode 100644 index 8b386cea..00000000 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/DefaultMongoFormats.scala +++ /dev/null @@ -1,38 +0,0 @@ -package io.sphere.mongo.generic - -import com.mongodb.BasicDBObject - -object DefaultMongoFormats extends DefaultMongoFormats {} - -trait DefaultMongoFormats { - given TypedMongoFormat[Int] = new NativeMongoFormat[Int] - - given TypedMongoFormat[String] = new NativeMongoFormat[String] - - given TypedMongoFormat[Boolean] = new NativeMongoFormat[Boolean] - - given [A](using TypedMongoFormat[A]): TypedMongoFormat[Option[A]] = - new TypedMongoFormat[Option[A]] { - override def toMongoValue(a: Option[A]): MongoType = - a match { - case Some(value) => summon[TypedMongoFormat[A]].toMongoValue(value) - case None => MongoNothing - } - - override def fromMongoValue(mongoType: MongoType): Option[A] = { - val fieldNames = summon[TypedMongoFormat[A]].fieldNames - if (mongoType == null) None - else - mongoType match { - case s: SimpleMongoType => Some(summon[TypedMongoFormat[A]].fromMongoValue(s)) - case bson: BasicDBObject => - val bsonFieldNames = bson.keySet().toArray - if (fieldNames.nonEmpty && bsonFieldNames.intersect(fieldNames).isEmpty) None - else Some(summon[TypedMongoFormat[A]].fromMongoValue(bson)) - case MongoNothing => None // This can't happen, but it makes the compiler happy - } - } - - override def default: Option[Option[A]] = Some(None) - } -} diff --git a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala b/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala deleted file mode 100644 index 67adad5f..00000000 --- a/mongo/mongo-derivation-scala-3/src/main/scala/io/sphere/mongo/generic/Derivation.scala +++ /dev/null @@ -1,152 +0,0 @@ -package io.sphere.mongo.generic - -import com.mongodb.BasicDBObject -import org.bson.types.ObjectId - -import java.util.UUID -import java.util.regex.Pattern -import scala.deriving.Mirror - -object MongoNothing -type SimpleMongoType = UUID | String | ObjectId | Short | Int | Long | Float | Double | Boolean | - Pattern -type MongoType = BasicDBObject | SimpleMongoType | MongoNothing.type - -trait TypedMongoFormat[A] extends Serializable { - def toMongoValue(a: A): MongoType - def fromMongoValue(mongoType: MongoType): A - -// /** needed JSON fields - ignored if empty */ - val fieldNames: Vector[String] = TypedMongoFormat.emptyFieldsSet - - def default: Option[A] = None -} - -private final class NativeMongoFormat[A <: SimpleMongoType] extends TypedMongoFormat[A] { - def toMongoValue(a: A): MongoType = a - def fromMongoValue(any: MongoType): A = any.asInstanceOf[A] -} - -inline def deriveMongoFormat[A](using Mirror.Of[A]): TypedMongoFormat[A] = TypedMongoFormat.derived - -object TypedMongoFormat { - inline def apply[A: TypedMongoFormat]: TypedMongoFormat[A] = summon - - private val emptyFieldsSet: Vector[String] = Vector.empty - - inline given derived[A](using Mirror.Of[A]): TypedMongoFormat[A] = Derivation.derived - - private def addField(bson: BasicDBObject, field: Field, mongoType: MongoType): Unit = - if (!field.ignored) - mongoType match { - case s: SimpleMongoType => bson.put(field.name, s) - case innerBson: BasicDBObject => - if (field.embedded) innerBson.entrySet().forEach(p => bson.put(p.getKey, p.getValue)) - else bson.put(field.name, innerBson) - case MongoNothing => - } - - private object Derivation { - - import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} - - inline def derived[A](using m: Mirror.Of[A]): TypedMongoFormat[A] = - inline m match { - case s: Mirror.SumOf[A] => deriveTrait(s) - case p: Mirror.ProductOf[A] => deriveCaseClass(p) - } - - inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): TypedMongoFormat[A] = - new TypedMongoFormat[A] { - private val traitMetaData = AnnotationReader.readTraitMetaData[A] - private val typeHintMap = traitMetaData.subtypes.collect { - case (name, classMeta) if classMeta.typeHint.isDefined => - name -> classMeta.typeHint.get - } - private val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) - private val formatters = summonFormatters[mirrorOfSum.MirroredElemTypes] - private val names = constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector - .asInstanceOf[Vector[String]] - private val formattersByTypeName = names.zip(formatters).toMap - - override def toMongoValue(a: A): MongoType = { - // we never get a trait here, only classes, it's safe to assume Product - val originalTypeName = a.asInstanceOf[Product].productPrefix - val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) - val bson = - formattersByTypeName(originalTypeName).toMongoValue(a).asInstanceOf[BasicDBObject] - bson.put(traitMetaData.typeDiscriminator, typeName) - bson - } - - override def fromMongoValue(bson: MongoType): A = - bson match { - case bson: BasicDBObject => - val typeName = bson.get(traitMetaData.typeDiscriminator).asInstanceOf[String] - val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - formattersByTypeName(originalTypeName).fromMongoValue(bson).asInstanceOf[A] - case x => - throw new Exception(s"BsonObject is expected for a Trait subtype, instead got $x") - } - } - - inline private def deriveCaseClass[A]( - mirrorOfProduct: Mirror.ProductOf[A]): TypedMongoFormat[A] = new TypedMongoFormat[A] { - private val caseClassMetaData = AnnotationReader.readCaseClassMetaData[A] - private val formatters = summonFormatters[mirrorOfProduct.MirroredElemTypes] - private val fieldsAndFormatters = caseClassMetaData.fields.zip(formatters) - - override val fieldNames: Vector[String] = fieldsAndFormatters.flatMap((field, formatter) => - if (field.embedded) formatter.fieldNames :+ field.rawName - else Vector(field.rawName)) - - override def toMongoValue(a: A): MongoType = { - val bson = new BasicDBObject() - val values = a.asInstanceOf[Product].productIterator - formatters.zip(values).zip(caseClassMetaData.fields).foreach { - case ((format, value), field) => - addField(bson, field, format.toMongoValue(value)) - } - bson - } - - override def fromMongoValue(mongoType: MongoType): A = - mongoType match { - case bson: BasicDBObject => - val fields = fieldsAndFormatters - .map { case (field, format) => - def defaultValue = field.defaultArgument.orElse(format.default) - - if (field.ignored) - defaultValue.getOrElse { - throw new Exception( - s"Missing default parameter value for ignored field `${field.name}` on deserialization.") - } - else if (field.embedded) format.fromMongoValue(bson) - else { - val value = bson.get(field.name) - if (value ne null) format.fromMongoValue(value.asInstanceOf[MongoType]) - else - defaultValue.getOrElse { - throw new Exception( - s"Missing required field '${field.name}' on deserialization.") - } - } - } - val tuple = Tuple.fromArray(fields.toArray) - mirrorOfProduct.fromTuple(tuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) - - case x => throw new Exception(s"BasicDBObject is expected for a class, instead got: $x") - } - } - - inline private def summonFormatters[T <: Tuple]: Vector[TypedMongoFormat[Any]] = - inline erasedValue[T] match { - case _: EmptyTuple => Vector.empty - case _: (t *: ts) => - summonInline[TypedMongoFormat[t]] - .asInstanceOf[TypedMongoFormat[Any]] +: summonFormatters[ts] - } - - } -} From 3a509a502187a43d145abfb18be24ca15ed5d461 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Mon, 25 Nov 2024 11:04:47 +0100 Subject: [PATCH 042/142] Port DefaultMongoFormats to mongo-3 --- .../mongo/format/DefaultMongoFormats.scala | 270 ++++++++++++++++++ .../mongo/generic/DefaultMongoFormats.scala | 39 --- .../io/sphere/mongo/DerivationSpec.scala | 2 +- .../io/sphere/mongo/SerializationTest.scala | 6 +- .../MongoFormatCatsInstancesTest.scala | 29 ++ .../format/DefaultMongoFormatsTest.scala | 149 ++++++++++ .../mongo/format/OptionMongoFormatSpec.scala | 2 +- .../mongo/generic/DefaultValuesSpec.scala | 2 +- .../mongo/generic/DeriveMongoFormatSpec.scala | 2 +- .../mongo/generic/MongoEmbeddedSpec.scala | 2 +- .../mongo/generic/MongoIgnoreSpec.scala | 2 +- .../sphere/mongo/generic/MongoKeySpec.scala | 2 +- ...goTypeHintFieldWithAbstractClassSpec.scala | 2 +- ...ongoTypeHintFieldWithSealedTraitSpec.scala | 2 +- .../mongo/generic/SumTypesDerivingSpec.scala | 2 +- .../MongoFormatCatsInstancesTest.scala | 6 - 16 files changed, 461 insertions(+), 58 deletions(-) create mode 100644 mongo/mongo-3/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala delete mode 100644 mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/DefaultMongoFormats.scala create mode 100644 mongo/mongo-3/src/test/scala/io/sphere/mongo/catsinstances/MongoFormatCatsInstancesTest.scala create mode 100644 mongo/mongo-3/src/test/scala/io/sphere/mongo/format/DefaultMongoFormatsTest.scala diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala new file mode 100644 index 00000000..6f601264 --- /dev/null +++ b/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala @@ -0,0 +1,270 @@ +package io.sphere.mongo.format + +import com.mongodb.BasicDBObject +import io.sphere.mongo.format +import io.sphere.mongo.format.SimpleMongoType +import io.sphere.util.{BaseMoney, HighPrecisionMoney, LangTag, Money} +import org.bson.{BSONObject, BasicBSONObject} +import org.bson.types.{BasicBSONList, ObjectId} + +import java.util.{Currency, Locale, UUID} +import java.util.regex.Pattern +import scala.collection.immutable.VectorBuilder +import scala.collection.mutable.ListBuffer + +object DefaultMongoFormats extends DefaultMongoFormats {} + +trait DefaultMongoFormats { + given uuidFormat: MongoFormat[UUID] = new NativeMongoFormat[UUID] + given objectIdFormat: MongoFormat[ObjectId] = new NativeMongoFormat[ObjectId] + given stringFormat: MongoFormat[String] = new NativeMongoFormat[String] + given shortFormat: MongoFormat[Short] = new NativeMongoFormat[Short] + given intFormat: MongoFormat[Int] = new NativeMongoFormat[Int] + given longFormat: MongoFormat[Long] = new MongoFormat[Long] { + private val native = new NativeMongoFormat[Long] + + override def toMongoValue(a: Long): Any = native.toMongoValue(a) + + override def fromMongoValue(any: Any): Long = + any match { + // a Long can read from an Int (for example, old aggregates version) + case i: Int => intFormat.fromMongoValue(i) + case _ => native.fromMongoValue(any) + } + } + given floatFormat: MongoFormat[Float] = new NativeMongoFormat[Float] + given doubleFormat: MongoFormat[Double] = new NativeMongoFormat[Double] + given booleanFormat: MongoFormat[Boolean] = new NativeMongoFormat[Boolean] + given patternFormat: MongoFormat[Pattern] = new NativeMongoFormat[Pattern] + + given optionFormat[A](using format: MongoFormat[A]): MongoFormat[Option[A]] = + new MongoFormat[Option[A]] { + override def toMongoValue(a: Option[A]): Any = + a match { + case Some(value) => format.toMongoValue(value) + case None => MongoNothing + } + + override def fromMongoValue(mongoType: Any): Option[A] = { + val fieldNames = format.fieldNames + if (mongoType == null) None + else + mongoType match { + case s: SimpleMongoType => Some(format.fromMongoValue(s)) + case bson: BasicDBObject => + val bsonFieldNames = bson.keySet().toArray + if (fieldNames.nonEmpty && bsonFieldNames.intersect(fieldNames).isEmpty) None + else Some(format.fromMongoValue(bson)) + case MongoNothing => None // This can't happen, but it makes the compiler happy + } + } + + override def default: Option[Option[A]] = Some(None) + } + + given vecFormat[@specialized A](using format: MongoFormat[A]): MongoFormat[Vector[A]] = + new MongoFormat[Vector[A]] { + import scala.jdk.CollectionConverters._ + override def toMongoValue(a: Vector[A]) = { + val m = new BasicBSONList() + if (a.nonEmpty) + m.addAll(a.map(format.toMongoValue(_).asInstanceOf[AnyRef]).asJavaCollection) + m + } + override def fromMongoValue(any: Any): Vector[A] = + any match { + case l: BasicBSONList => + if (l.isEmpty) Vector.empty + else { + val builder = new VectorBuilder[A] + val iter = l.iterator() + while (iter.hasNext) { + val element = iter.next() + builder += format.fromMongoValue(element) + } + builder.result() + } + case _ => throw new Exception(s"cannot read value from ${any.getClass.getName}") + } + } + + given listFormat[@specialized A](using format: MongoFormat[A]): MongoFormat[List[A]] = + new MongoFormat[List[A]] { + import scala.jdk.CollectionConverters._ + override def toMongoValue(a: List[A]) = { + val m = new BasicBSONList() + if (a.nonEmpty) + m.addAll(a.map(format.toMongoValue(_).asInstanceOf[AnyRef]).asJavaCollection) + m + } + override def fromMongoValue(any: Any): List[A] = + any match { + case l: BasicBSONList => + if (l.isEmpty) Nil + else { + val builder = new ListBuffer[A] + val iter = l.iterator() + while (iter.hasNext) { + val element = iter.next() + builder += format.fromMongoValue(element) + } + builder.result() + } + case _ => throw new Exception(s"cannot read value from ${any.getClass.getName}") + } + } + + given setFormat[@specialized A](using f: MongoFormat[A]): MongoFormat[Set[A]] = + new MongoFormat[Set[A]] { + import scala.jdk.CollectionConverters._ + override def toMongoValue(a: Set[A]) = { + val m = new BasicBSONList() + if (a.nonEmpty) + m.addAll(a.map(f.toMongoValue(_).asInstanceOf[AnyRef]).asJavaCollection) + m + } + override def fromMongoValue(any: Any): Set[A] = + any match { + case l: BasicBSONList => + if (l.isEmpty) Set.empty + else l.iterator().asScala.map(f.fromMongoValue).toSet + case _ => throw new Exception(s"cannot read value from ${any.getClass.getName}") + } + } + + given mapFormat[@specialized A](using f: MongoFormat[A]): MongoFormat[Map[String, A]] = + new MongoFormat[Map[String, A]] { + override def toMongoValue(map: Map[String, A]): Any = + // Perf note: new BasicBSONObject(map.size) is much slower for some reason + map.foldLeft(new BasicBSONObject()) { case (dbo, (k, v)) => + dbo.append(k, summon[MongoFormat[A]].toMongoValue(v)) + } + + override def fromMongoValue(any: Any): Map[String, A] = { + import scala.language.existentials + + val map: java.util.Map[_, _] = any match { + case b: BasicBSONObject => b // avoid instantiating a new map + case dbo: BSONObject => dbo.toMap + case other => throw new Exception(s"cannot read value from ${other.getClass.getName}") + } + val builder = Map.newBuilder[String, A] + val iter = map.entrySet().iterator() + while (iter.hasNext) { + val entry = iter.next() + val k = entry.getKey.asInstanceOf[String] + val v = summon[MongoFormat[A]].fromMongoValue(entry.getValue) + builder += (k -> v) + } + builder.result() + } + } + + given currencyFormat: MongoFormat[Currency] = new MongoFormat[Currency] { + val failMsg = "ISO 4217 code JSON String expected." + def failMsgFor(input: String) = s"Currency '$input' not valid as ISO 4217 code." + + override def toMongoValue(c: Currency): Any = c.getCurrencyCode + override def fromMongoValue(any: Any): Currency = any match { + case s: String => + try Currency.getInstance(s) + catch { + case _: IllegalArgumentException => throw new Exception(failMsgFor(s)) + } + case _ => throw new Exception(failMsg) + } + } + + given moneyFormat: MongoFormat[Money] = new MongoFormat[Money] { + import Money._ + + override val fieldNames = Vector(CentAmountField, CurrencyCodeField) + + override def toMongoValue(m: Money): Any = + new BasicBSONObject() + .append(BaseMoney.TypeField, m.`type`) + .append(CurrencyCodeField, currencyFormat.toMongoValue(m.currency)) + .append(CentAmountField, longFormat.toMongoValue(m.centAmount)) + .append(FractionDigitsField, m.currency.getDefaultFractionDigits) + + override def fromMongoValue(any: Any): Money = any match { + case dbo: BSONObject => + Money.fromCentAmount( + field[Long](CentAmountField, dbo), + field[Currency](CurrencyCodeField, dbo)) + case other => throw new Exception(s"db object expected but has '${other.getClass.getName}'") + } + } + + given highPrecisionMoneyFormat: MongoFormat[HighPrecisionMoney] = + new MongoFormat[HighPrecisionMoney] { + import HighPrecisionMoney._ + + override val fieldNames = Vector(PreciseAmountField, CurrencyCodeField, FractionDigitsField) + + override def toMongoValue(m: HighPrecisionMoney): Any = + new BasicBSONObject() + .append(BaseMoney.TypeField, m.`type`) + .append(CurrencyCodeField, currencyFormat.toMongoValue(m.currency)) + .append(CentAmountField, longFormat.toMongoValue(m.centAmount)) + .append(PreciseAmountField, longFormat.toMongoValue(m.preciseAmount)) + .append(FractionDigitsField, m.fractionDigits) + override def fromMongoValue(any: Any): HighPrecisionMoney = any match { + case dbo: BSONObject => + HighPrecisionMoney + .fromPreciseAmount( + field[Long](PreciseAmountField, dbo), + field[Int](FractionDigitsField, dbo), + field[Currency](CurrencyCodeField, dbo), + field[Option[Long]](CentAmountField, dbo) + ) + .fold(nel => throw new Exception(nel.toList.mkString(", ")), identity) + + case other => throw new Exception(s"db object expected but has '${other.getClass.getName}'") + } + } + + given baseMoneyFormat: MongoFormat[BaseMoney] = new MongoFormat[BaseMoney] { + override def toMongoValue(a: BaseMoney): Any = a match { + case m: Money => moneyFormat.toMongoValue(m) + case m: HighPrecisionMoney => highPrecisionMoneyFormat.toMongoValue(m) + } + override def fromMongoValue(any: Any): BaseMoney = any match { + case dbo: BSONObject => + val typeField = dbo.get(BaseMoney.TypeField) + if (typeField == null) + moneyFormat.fromMongoValue(any) + else + stringFormat.fromMongoValue(typeField) match { + case Money.TypeName => moneyFormat.fromMongoValue(any) + case HighPrecisionMoney.TypeName => + highPrecisionMoneyFormat.fromMongoValue(any) + case tpe => + throw new Exception( + s"Unknown money type '$tpe'. Available types are: '${Money.TypeName}', '${HighPrecisionMoney.TypeName}'.") + } + case other => throw new Exception(s"db object expected but has '${other.getClass.getName}'") + } + } + + given localeFormat: MongoFormat[Locale] = new MongoFormat[Locale] { + override def toMongoValue(a: Locale): Any = a.toLanguageTag + override def fromMongoValue(any: Any): Locale = any match { + case s: String => + s match { + case LangTag(langTag) => langTag + case _ => + if (LangTag.unapply(s).isEmpty) + throw new Exception("Undefined locale is not allowed") + else + throw new Exception(LangTag.invalidLangTagMessage(s)) + } + case _ => + throw new Exception( + s"Locale is expected to be of type String but has '${any.getClass.getName}'") + } + } + + private def field[A](name: String, dbo: BSONObject)(implicit format: MongoFormat[A]): A = + format.fromMongoValue(dbo.get(name)) +} diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/DefaultMongoFormats.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/DefaultMongoFormats.scala deleted file mode 100644 index 0ce5de71..00000000 --- a/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/DefaultMongoFormats.scala +++ /dev/null @@ -1,39 +0,0 @@ -package io.sphere.mongo.generic - -import com.mongodb.BasicDBObject -import io.sphere.mongo.format.{MongoFormat, MongoNothing, NativeMongoFormat, SimpleMongoType} - -object DefaultMongoFormats extends DefaultMongoFormats {} - -trait DefaultMongoFormats { - given MongoFormat[Int] = new NativeMongoFormat[Int] - - given MongoFormat[String] = new NativeMongoFormat[String] - - given MongoFormat[Boolean] = new NativeMongoFormat[Boolean] - - given [A](using MongoFormat[A]): MongoFormat[Option[A]] = - new MongoFormat[Option[A]] { - override def toMongoValue(a: Option[A]): Any = - a match { - case Some(value) => summon[MongoFormat[A]].toMongoValue(value) - case None => MongoNothing - } - - override def fromMongoValue(mongoType: Any): Option[A] = { - val fieldNames = summon[MongoFormat[A]].fieldNames - if (mongoType == null) None - else - mongoType match { - case s: SimpleMongoType => Some(summon[MongoFormat[A]].fromMongoValue(s)) - case bson: BasicDBObject => - val bsonFieldNames = bson.keySet().toArray - if (fieldNames.nonEmpty && bsonFieldNames.intersect(fieldNames).isEmpty) None - else Some(summon[MongoFormat[A]].fromMongoValue(bson)) - case MongoNothing => None // This can't happen, but it makes the compiler happy - } - } - - override def default: Option[Option[A]] = Some(None) - } -} diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala index f7e074be..b20c5346 100644 --- a/mongo/mongo-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala @@ -6,7 +6,7 @@ import io.sphere.mongo.generic.{ MongoKey, MongoTypeHintField } -import io.sphere.mongo.generic.DefaultMongoFormats.given +import io.sphere.mongo.format.DefaultMongoFormats.given import org.scalatest.wordspec.AnyWordSpec import org.scalatest.matchers.must.Matchers diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/SerializationTest.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/SerializationTest.scala index 9d221ca4..3af167c5 100644 --- a/mongo/mongo-3/src/test/scala/io/sphere/mongo/SerializationTest.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/SerializationTest.scala @@ -1,9 +1,9 @@ package io.sphere.mongo import com.mongodb.BasicDBObject -import io.sphere.mongo.format.MongoFormat -import io.sphere.mongo.generic.{AnnotationReader, DefaultMongoFormats} -import io.sphere.mongo.generic.DefaultMongoFormats.given +import io.sphere.mongo.format.{DefaultMongoFormats, MongoFormat} +import io.sphere.mongo.generic.AnnotationReader +import DefaultMongoFormats.given import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/catsinstances/MongoFormatCatsInstancesTest.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/catsinstances/MongoFormatCatsInstancesTest.scala new file mode 100644 index 00000000..34fbb491 --- /dev/null +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/catsinstances/MongoFormatCatsInstancesTest.scala @@ -0,0 +1,29 @@ +package io.sphere.mongo.catsinstances + +import cats.syntax.invariant.* +import io.sphere.mongo.{fromMongo, toMongo} +import io.sphere.mongo.format.* +import io.sphere.mongo.format.DefaultMongoFormats.given +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class MongoFormatCatsInstancesTest extends AnyWordSpec with Matchers { + import MongoFormatCatsInstancesTest.* + + "Invariant[MongoFormat]" must { + "allow imaping a default format" in { + val myId = MyId("test") + val dbo = toMongo(myId) + dbo.asInstanceOf[String] must be("test") + val myNewId = fromMongo[MyId](dbo) + myNewId must be(myId) + } + } +} + +object MongoFormatCatsInstancesTest { + case class MyId(id: String) extends AnyVal + object MyId { + implicit val mongo: MongoFormat[MyId] = MongoFormat[String].imap(MyId.apply)(_.id) + } +} diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/format/DefaultMongoFormatsTest.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/format/DefaultMongoFormatsTest.scala new file mode 100644 index 00000000..fd97ae26 --- /dev/null +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/format/DefaultMongoFormatsTest.scala @@ -0,0 +1,149 @@ +package io.sphere.mongo.format + +import com.mongodb.DBObject +import io.sphere.mongo.MongoUtils +import io.sphere.mongo.format.DefaultMongoFormats.given +import io.sphere.util.LangTag +import org.bson.BasicBSONObject +import org.bson.types.BasicBSONList +import org.scalacheck.Gen +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec +import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks + +import java.util.Locale +import scala.jdk.CollectionConverters.* + +object DefaultMongoFormatsTest { + case class User(name: String) + object User { + implicit val mongo: MongoFormat[User] = new MongoFormat[User] { + override def toMongoValue(a: User): Any = MongoUtils.dbObj("name" -> a.name) + override def fromMongoValue(any: Any): User = any match { + case dbo: DBObject => + User(dbo.get("name").asInstanceOf[String]) + case _ => throw new Exception("expected DBObject") + } + } + } +} + +class DefaultMongoFormatsTest + extends AnyWordSpec + with Matchers + with ScalaCheckDrivenPropertyChecks { + import DefaultMongoFormatsTest.* + + "DefaultMongoFormats" must { + "support List[String]" in { + val format = listFormat[String] + val list = Gen.listOf(Gen.alphaNumStr) + + forAll(list) { l => + val dbo = format.toMongoValue(l) + dbo.asInstanceOf[BasicBSONList].asScala.toList must be(l) + val resultList = format.fromMongoValue(dbo) + resultList must be(l) + } + } + + "support List[A: MongoFormat]" in { + val format = listFormat[User] + val list = Gen.listOf(Gen.alphaNumStr.map(User.apply)) + + check(list, format) + } + + "support Vector[String]" in { + val format = vecFormat[String] + val vector = Gen.listOf(Gen.alphaNumStr).map(_.toVector) + + forAll(vector) { v => + val dbo = format.toMongoValue(v) + dbo.asInstanceOf[BasicBSONList].asScala.toVector must be(v) + val resultVector = format.fromMongoValue(dbo) + resultVector must be(v) + } + } + + "support Vector[A: MongoFormat]" in { + val format = vecFormat[User] + val vector = Gen.listOf(Gen.alphaNumStr.map(User.apply)).map(_.toVector) + + check(vector, format) + } + + "support Set[String]" in { + val format = setFormat[String] + val set = Gen.listOf(Gen.alphaNumStr).map(_.toSet) + + forAll(set) { s => + val dbo = format.toMongoValue(s) + dbo.asInstanceOf[BasicBSONList].asScala.toSet must be(s) + val resultSet = format.fromMongoValue(dbo) + resultSet must be(s) + } + } + + "support Set[A: MongoFormat]" in { + val format = setFormat[User] + val set = Gen.listOf(Gen.alphaNumStr.map(User.apply)).map(_.toSet) + + check(set, format) + } + + "support Map[String, String]" in { + val format = mapFormat[String] + val map = Gen + .listOf { + for { + key <- Gen.alphaNumStr + value <- Gen.alphaNumStr + } yield (key, value) + } + .map(_.toMap) + + forAll(map) { m => + val dbo = format.toMongoValue(m) + dbo.asInstanceOf[BasicBSONObject].asScala must be(m) + val resultMap = format.fromMongoValue(dbo) + resultMap must be(m) + } + } + + "support Map[String, A: MongoFormat]" in { + val format = mapFormat[User] + val map = Gen + .listOf { + for { + key <- Gen.alphaNumStr + value <- Gen.alphaNumStr.map(User.apply) + } yield (key, value) + } + .map(_.toMap) + + check(map, format) + } + + "support Java Locale" in { + Locale.getAvailableLocales.filter(_.toLanguageTag != LangTag.UNDEFINED).foreach { l => + localeFormat.fromMongoValue(localeFormat.toMongoValue(l)).toLanguageTag must be( + l.toLanguageTag) + } + } + + "support UUID" in { + val format = uuidFormat + val uuids = Gen.uuid + + check(uuids, format) + } + } + + private def check[A](gen: Gen[A], format: MongoFormat[A]) = + forAll(gen) { value => + val dbo = format.toMongoValue(value) + val result = format.fromMongoValue(dbo) + result must be(value) + } +} diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala index 40e99c4f..cf56d827 100644 --- a/mongo/mongo-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala @@ -2,7 +2,7 @@ package io.sphere.mongo.format import io.sphere.mongo.MongoUtils.* import io.sphere.mongo.generic.* -import io.sphere.mongo.generic.DefaultMongoFormats.given +import DefaultMongoFormats.given import org.scalatest.OptionValues import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala index b5b4cd21..e481fff7 100644 --- a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala @@ -1,7 +1,7 @@ package io.sphere.mongo.generic import io.sphere.mongo.MongoUtils.* -import io.sphere.mongo.generic.DefaultMongoFormats.given +import io.sphere.mongo.format.DefaultMongoFormats.given import io.sphere.mongo.format.{MongoFormat, deriveMongoFormat} import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala index 8ac56f87..01fe73bd 100644 --- a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala @@ -2,7 +2,7 @@ package io.sphere.mongo.generic import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec -import io.sphere.mongo.generic.DefaultMongoFormats.given +import io.sphere.mongo.format.DefaultMongoFormats.given import io.sphere.mongo.MongoUtils.* import io.sphere.mongo.format.{MongoFormat, deriveMongoFormat} import io.sphere.mongo.{fromMongo, toMongo} diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala index 741152bd..b46493c6 100644 --- a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala @@ -2,7 +2,7 @@ package io.sphere.mongo.generic import io.sphere.mongo.MongoUtils.* import io.sphere.mongo.format.deriveMongoFormat -import io.sphere.mongo.generic.DefaultMongoFormats.given +import io.sphere.mongo.format.DefaultMongoFormats.given import org.scalatest.OptionValues import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoIgnoreSpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoIgnoreSpec.scala index dcda4d6f..58bedee2 100644 --- a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoIgnoreSpec.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoIgnoreSpec.scala @@ -2,7 +2,7 @@ package io.sphere.mongo.generic import io.sphere.mongo.MongoUtils.* import io.sphere.mongo.format.deriveMongoFormat -import io.sphere.mongo.generic.DefaultMongoFormats.given +import io.sphere.mongo.format.DefaultMongoFormats.given import org.scalatest.OptionValues import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala index d022a1f0..a51d3d98 100644 --- a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala @@ -2,7 +2,7 @@ package io.sphere.mongo.generic import com.mongodb.BasicDBObject import io.sphere.mongo.format.deriveMongoFormat -import io.sphere.mongo.generic.DefaultMongoFormats.given +import io.sphere.mongo.format.DefaultMongoFormats.given import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala index b820a7aa..db7e22c6 100644 --- a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala @@ -2,7 +2,7 @@ package io.sphere.mongo.generic import io.sphere.mongo.MongoUtils.dbObj import io.sphere.mongo.format.{MongoFormat, deriveMongoFormat} -import io.sphere.mongo.generic.DefaultMongoFormats.given +import io.sphere.mongo.format.DefaultMongoFormats.given import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala index 9592f1cd..49a61fb4 100644 --- a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala @@ -2,7 +2,7 @@ package io.sphere.mongo.generic import io.sphere.mongo.MongoUtils.dbObj import io.sphere.mongo.format.{MongoFormat, deriveMongoFormat} -import io.sphere.mongo.generic.DefaultMongoFormats.given +import io.sphere.mongo.format.DefaultMongoFormats.given import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala index dd31c9a8..3dd8944e 100644 --- a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala @@ -3,7 +3,7 @@ package io.sphere.mongo.generic import com.mongodb.DBObject import io.sphere.mongo.MongoUtils.dbObj import io.sphere.mongo.format.{MongoFormat, deriveMongoFormat} -import io.sphere.mongo.generic.DefaultMongoFormats.given +import io.sphere.mongo.format.DefaultMongoFormats.given import org.bson.BSONObject import org.scalatest.Assertion import org.scalatest.matchers.must.Matchers diff --git a/mongo/mongo-core/src/test/scala/io/sphere/mongo/catsinstances/MongoFormatCatsInstancesTest.scala b/mongo/mongo-core/src/test/scala/io/sphere/mongo/catsinstances/MongoFormatCatsInstancesTest.scala index 99b7b4b8..47f37647 100644 --- a/mongo/mongo-core/src/test/scala/io/sphere/mongo/catsinstances/MongoFormatCatsInstancesTest.scala +++ b/mongo/mongo-core/src/test/scala/io/sphere/mongo/catsinstances/MongoFormatCatsInstancesTest.scala @@ -1,7 +1,6 @@ package io.sphere.mongo.catsinstances import cats.syntax.invariant._ -import io.sphere.mongo.Test import io.sphere.mongo.format.DefaultMongoFormats._ import io.sphere.mongo.format._ import org.scalatest.matchers.must.Matchers @@ -19,11 +18,6 @@ class MongoFormatCatsInstancesTest extends AnyWordSpec with Matchers { myNewId must be(myId) } } - - "asd" in { - println("asdd") - Test.fn - } } object MongoFormatCatsInstancesTest { From bd0355113b897a89fecdbb8fc2b6d85094991ce2 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Mon, 25 Nov 2024 11:12:30 +0100 Subject: [PATCH 043/142] Add BaseMoneyMongoFormatTest --- .../mongo/format/DefaultMongoFormats.scala | 2 +- .../format/BaseMoneyMongoFormatTest.scala | 97 +++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 mongo/mongo-3/src/test/scala/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala index 6f601264..61dd1915 100644 --- a/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala +++ b/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala @@ -143,7 +143,7 @@ trait DefaultMongoFormats { override def fromMongoValue(any: Any): Map[String, A] = { import scala.language.existentials - val map: java.util.Map[_, _] = any match { + val map: java.util.Map[?, ?] = any match { case b: BasicBSONObject => b // avoid instantiating a new map case dbo: BSONObject => dbo.toMap case other => throw new Exception(s"cannot read value from ${other.getClass.getName}") diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala new file mode 100644 index 00000000..aeac2be2 --- /dev/null +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala @@ -0,0 +1,97 @@ +package io.sphere.mongo.format + +import io.sphere.mongo.MongoUtils.* +import io.sphere.mongo.format.DefaultMongoFormats.given +import io.sphere.util.{BaseMoney, HighPrecisionMoney, Money} +import org.bson.BSONObject +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import java.util.Currency +import scala.jdk.CollectionConverters.* + +class BaseMoneyMongoFormatTest extends AnyWordSpec with Matchers { + + "MongoFormat[BaseMoney]" should { + "be symmetric" in { + val money = Money.EUR(34.56) + val f = MongoFormat[Money] + val dbo = f.toMongoValue(money) + val readMoney = f.fromMongoValue(dbo) + + money should be(readMoney) + } + + "decode with type info" in { + val dbo = dbObj( + "type" -> "centPrecision", + "currencyCode" -> "USD", + "centAmount" -> 3298 + ) + + MongoFormat[BaseMoney].fromMongoValue(dbo) should be(Money.USD(BigDecimal("32.98"))) + } + + "decode without type info" in { + val dbo = dbObj( + "currencyCode" -> "USD", + "centAmount" -> 3298 + ) + + MongoFormat[BaseMoney].fromMongoValue(dbo) should be(Money.USD(BigDecimal("32.98"))) + } + } + + "MongoFormat[HighPrecisionMoney]" should { + "be symmetric" in { + implicit val mode = BigDecimal.RoundingMode.HALF_EVEN + + val money = HighPrecisionMoney.fromDecimalAmount(34.123456, 6, Currency.getInstance("EUR")) + val dbo = MongoFormat[HighPrecisionMoney].toMongoValue(money) + + val decodedMoney = MongoFormat[HighPrecisionMoney].fromMongoValue(dbo) + val decodedBaseMoney = MongoFormat[BaseMoney].fromMongoValue(dbo) + + decodedMoney should equal(money) + decodedBaseMoney should equal(money) + } + + "decode with type info" in { + val dbo = dbObj( + "type" -> "highPrecision", + "currencyCode" -> "USD", + "preciseAmount" -> 42, + "fractionDigits" -> 4 + ) + + MongoFormat[BaseMoney].fromMongoValue(dbo) should be( + HighPrecisionMoney.USD(BigDecimal("0.0042"), Some(4))) + } + + "decode with centAmount" in { + val dbo = dbObj( + "type" -> "highPrecision", + "currencyCode" -> "USD", + "preciseAmount" -> 42, + "centAmount" -> 1, + "fractionDigits" -> 4 + ) + + val parsed = MongoFormat[BaseMoney].fromMongoValue(dbo) + MongoFormat[BaseMoney].toMongoValue(parsed).asInstanceOf[BSONObject].toMap.asScala should be( + dbo.toMap.asScala) + } + + "validate data when decoded from JSON" in { + val dbo = dbObj( + "type" -> "highPrecision", + "currencyCode" -> "USD", + "preciseAmount" -> 42, + "fractionDigits" -> 1 + ) + + an[Exception] shouldBe thrownBy(MongoFormat[BaseMoney].fromMongoValue(dbo)) + } + } + +} From a8490a8d5cca1a1e7a4d9cd79f008eb3b98f144f Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Mon, 25 Nov 2024 12:53:10 +0100 Subject: [PATCH 044/142] Remove "New anonymous class definition will be duplicated at each inline site" warnings. --- .../io/sphere/mongo/format/MongoFormat.scala | 129 +++++++++--------- .../mongo/generic/AnnotationReader.scala | 4 + 2 files changed, 72 insertions(+), 61 deletions(-) diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/MongoFormat.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/MongoFormat.scala index ffcf9856..a1ee6577 100644 --- a/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/MongoFormat.scala +++ b/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/MongoFormat.scala @@ -1,7 +1,7 @@ package io.sphere.mongo.format import com.mongodb.BasicDBObject -import io.sphere.mongo.generic.{AnnotationReader, Field} +import io.sphere.mongo.generic.{AnnotationReader, Field, TraitMetaData} import org.bson.types.ObjectId import java.util.UUID @@ -56,20 +56,17 @@ object MongoFormat { case p: Mirror.ProductOf[A] => deriveCaseClass(p) } - inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): MongoFormat[A] = - new MongoFormat[A] { - private val traitMetaData = AnnotationReader.readTraitMetaData[A] - private val typeHintMap = traitMetaData.subtypes.collect { - case (name, classMeta) if classMeta.typeHint.isDefined => - name -> classMeta.typeHint.get - } - private val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) - private val formatters = summonFormatters[mirrorOfSum.MirroredElemTypes] - private val names = constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector - .asInstanceOf[Vector[String]] - private val formattersByTypeName = names.zip(formatters).toMap - - override def toMongoValue(a: A): Any = { + inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): MongoFormat[A] = { + val traitMetaData = AnnotationReader.readTraitMetaData[A] + val typeHintMap = traitMetaData.subTypeTypeHints + val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) + val formatters = summonFormatters[mirrorOfSum.MirroredElemTypes] + val names = constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector + .asInstanceOf[Vector[String]] + val formattersByTypeName = names.zip(formatters).toMap + + MongoFormat.create[A]( + toMongo = { a => // we never get a trait here, only classes, it's safe to assume Product val originalTypeName = a.asInstanceOf[Product].productPrefix val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) @@ -77,30 +74,28 @@ object MongoFormat { formattersByTypeName(originalTypeName).toMongoValue(a).asInstanceOf[BasicDBObject] bson.put(traitMetaData.typeDiscriminator, typeName) bson + }, + fromMongo = { + case bson: BasicDBObject => + val typeName = bson.get(traitMetaData.typeDiscriminator).asInstanceOf[String] + val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) + formattersByTypeName(originalTypeName).fromMongoValue(bson).asInstanceOf[A] + case x => + throw new Exception(s"BsonObject is expected for a Trait subtype, instead got $x") } + ) + } - override def fromMongoValue(bson: Any): A = - bson match { - case bson: BasicDBObject => - val typeName = bson.get(traitMetaData.typeDiscriminator).asInstanceOf[String] - val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - formattersByTypeName(originalTypeName).fromMongoValue(bson).asInstanceOf[A] - case x => - throw new Exception(s"BsonObject is expected for a Trait subtype, instead got $x") - } - } - - inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): MongoFormat[A] = - new MongoFormat[A] { - private val caseClassMetaData = AnnotationReader.readCaseClassMetaData[A] - private val formatters = summonFormatters[mirrorOfProduct.MirroredElemTypes] - private val fieldsAndFormatters = caseClassMetaData.fields.zip(formatters) + inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): MongoFormat[A] = { + val caseClassMetaData = AnnotationReader.readCaseClassMetaData[A] + val formatters = summonFormatters[mirrorOfProduct.MirroredElemTypes] + val fieldsAndFormatters = caseClassMetaData.fields.zip(formatters) - override val fieldNames: Vector[String] = fieldsAndFormatters.flatMap((field, formatter) => + MongoFormat.create( + fields = fieldsAndFormatters.flatMap((field, formatter) => if (field.embedded) formatter.fieldNames :+ field.rawName - else Vector(field.rawName)) - - override def toMongoValue(a: A): Any = { + else Vector(field.rawName)), + toMongo = { a => val bson = new BasicDBObject() val values = a.asInstanceOf[Product].productIterator formatters.zip(values).zip(caseClassMetaData.fields).foreach { @@ -108,37 +103,36 @@ object MongoFormat { addField(bson, field, format.toMongoValue(value)) } bson - } - - override def fromMongoValue(mongoType: Any): A = - mongoType match { - case bson: BasicDBObject => - val fields = fieldsAndFormatters - .map { case (field, format) => - def defaultValue = field.defaultArgument.orElse(format.default) - - if (field.ignored) + }, + fromMongo = { + case bson: BasicDBObject => + val fields = fieldsAndFormatters + .map { (field, format) => + def defaultValue = field.defaultArgument.orElse(format.default) + + if (field.ignored) + defaultValue.getOrElse { + throw new Exception( + s"Missing default parameter value for ignored field `${field.name}` on deserialization.") + } + else if (field.embedded) format.fromMongoValue(bson) + else { + val value = bson.get(field.name) + if (value ne null) format.fromMongoValue(value.asInstanceOf[Any]) + else defaultValue.getOrElse { throw new Exception( - s"Missing default parameter value for ignored field `${field.name}` on deserialization.") + s"Missing required field '${field.name}' on deserialization.") } - else if (field.embedded) format.fromMongoValue(bson) - else { - val value = bson.get(field.name) - if (value ne null) format.fromMongoValue(value.asInstanceOf[Any]) - else - defaultValue.getOrElse { - throw new Exception( - s"Missing required field '${field.name}' on deserialization.") - } - } } - val tuple = Tuple.fromArray(fields.toArray) - mirrorOfProduct.fromTuple(tuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) + } + val tuple = Tuple.fromArray(fields.toArray) + mirrorOfProduct.fromTuple(tuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) - case x => throw new Exception(s"BasicDBObject is expected for a class, instead got: $x") - } - } + case x => throw new Exception(s"BasicDBObject is expected for a class, instead got: $x") + } + ) + } inline private def summonFormatters[T <: Tuple]: Vector[MongoFormat[Any]] = inline erasedValue[T] match { @@ -149,4 +143,17 @@ object MongoFormat { } } + + // This is needed to remove the "New anonymous class definition will be duplicated at each inline site" warnings + private def create[A]( + toMongo: A => Any, + fromMongo: Any => A, + fields: Vector[String] = MongoFormat.emptyFields): MongoFormat[A] = + new MongoFormat[A] { + override def toMongoValue(a: A): Any = toMongo(a) + + override def fromMongoValue(mongoType: Any): A = fromMongo(mongoType) + + override val fieldNames: Vector[String] = fields + } } diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala index 7b5c5a91..cc991d9d 100644 --- a/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala +++ b/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala @@ -28,6 +28,10 @@ case class TraitMetaData( subtypes: Map[String, CaseClassMetaData] ) { val typeDiscriminator: String = typeHintFieldRaw.map(_.value).getOrElse("type") + + val subTypeTypeHints: Map[String, String] = subtypes.collect { + case (name, classMeta) if classMeta.typeHint.isDefined => name -> classMeta.typeHint.get + } } object AnnotationReader { From 5c8db6994735c959bc299973c0586cc0b08cccfc Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Mon, 25 Nov 2024 13:06:17 +0100 Subject: [PATCH 045/142] Revert "Remove "New anonymous class definition will be duplicated at each inline site" warnings." This reverts commit a8490a8d5cca1a1e7a4d9cd79f008eb3b98f144f. --- .../io/sphere/mongo/format/MongoFormat.scala | 129 +++++++++--------- .../mongo/generic/AnnotationReader.scala | 4 - 2 files changed, 61 insertions(+), 72 deletions(-) diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/MongoFormat.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/MongoFormat.scala index a1ee6577..ffcf9856 100644 --- a/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/MongoFormat.scala +++ b/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/MongoFormat.scala @@ -1,7 +1,7 @@ package io.sphere.mongo.format import com.mongodb.BasicDBObject -import io.sphere.mongo.generic.{AnnotationReader, Field, TraitMetaData} +import io.sphere.mongo.generic.{AnnotationReader, Field} import org.bson.types.ObjectId import java.util.UUID @@ -56,17 +56,20 @@ object MongoFormat { case p: Mirror.ProductOf[A] => deriveCaseClass(p) } - inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): MongoFormat[A] = { - val traitMetaData = AnnotationReader.readTraitMetaData[A] - val typeHintMap = traitMetaData.subTypeTypeHints - val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) - val formatters = summonFormatters[mirrorOfSum.MirroredElemTypes] - val names = constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector - .asInstanceOf[Vector[String]] - val formattersByTypeName = names.zip(formatters).toMap - - MongoFormat.create[A]( - toMongo = { a => + inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): MongoFormat[A] = + new MongoFormat[A] { + private val traitMetaData = AnnotationReader.readTraitMetaData[A] + private val typeHintMap = traitMetaData.subtypes.collect { + case (name, classMeta) if classMeta.typeHint.isDefined => + name -> classMeta.typeHint.get + } + private val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) + private val formatters = summonFormatters[mirrorOfSum.MirroredElemTypes] + private val names = constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector + .asInstanceOf[Vector[String]] + private val formattersByTypeName = names.zip(formatters).toMap + + override def toMongoValue(a: A): Any = { // we never get a trait here, only classes, it's safe to assume Product val originalTypeName = a.asInstanceOf[Product].productPrefix val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) @@ -74,28 +77,30 @@ object MongoFormat { formattersByTypeName(originalTypeName).toMongoValue(a).asInstanceOf[BasicDBObject] bson.put(traitMetaData.typeDiscriminator, typeName) bson - }, - fromMongo = { - case bson: BasicDBObject => - val typeName = bson.get(traitMetaData.typeDiscriminator).asInstanceOf[String] - val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - formattersByTypeName(originalTypeName).fromMongoValue(bson).asInstanceOf[A] - case x => - throw new Exception(s"BsonObject is expected for a Trait subtype, instead got $x") } - ) - } - inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): MongoFormat[A] = { - val caseClassMetaData = AnnotationReader.readCaseClassMetaData[A] - val formatters = summonFormatters[mirrorOfProduct.MirroredElemTypes] - val fieldsAndFormatters = caseClassMetaData.fields.zip(formatters) + override def fromMongoValue(bson: Any): A = + bson match { + case bson: BasicDBObject => + val typeName = bson.get(traitMetaData.typeDiscriminator).asInstanceOf[String] + val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) + formattersByTypeName(originalTypeName).fromMongoValue(bson).asInstanceOf[A] + case x => + throw new Exception(s"BsonObject is expected for a Trait subtype, instead got $x") + } + } + + inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): MongoFormat[A] = + new MongoFormat[A] { + private val caseClassMetaData = AnnotationReader.readCaseClassMetaData[A] + private val formatters = summonFormatters[mirrorOfProduct.MirroredElemTypes] + private val fieldsAndFormatters = caseClassMetaData.fields.zip(formatters) - MongoFormat.create( - fields = fieldsAndFormatters.flatMap((field, formatter) => + override val fieldNames: Vector[String] = fieldsAndFormatters.flatMap((field, formatter) => if (field.embedded) formatter.fieldNames :+ field.rawName - else Vector(field.rawName)), - toMongo = { a => + else Vector(field.rawName)) + + override def toMongoValue(a: A): Any = { val bson = new BasicDBObject() val values = a.asInstanceOf[Product].productIterator formatters.zip(values).zip(caseClassMetaData.fields).foreach { @@ -103,36 +108,37 @@ object MongoFormat { addField(bson, field, format.toMongoValue(value)) } bson - }, - fromMongo = { - case bson: BasicDBObject => - val fields = fieldsAndFormatters - .map { (field, format) => - def defaultValue = field.defaultArgument.orElse(format.default) - - if (field.ignored) - defaultValue.getOrElse { - throw new Exception( - s"Missing default parameter value for ignored field `${field.name}` on deserialization.") - } - else if (field.embedded) format.fromMongoValue(bson) - else { - val value = bson.get(field.name) - if (value ne null) format.fromMongoValue(value.asInstanceOf[Any]) - else + } + + override def fromMongoValue(mongoType: Any): A = + mongoType match { + case bson: BasicDBObject => + val fields = fieldsAndFormatters + .map { case (field, format) => + def defaultValue = field.defaultArgument.orElse(format.default) + + if (field.ignored) defaultValue.getOrElse { throw new Exception( - s"Missing required field '${field.name}' on deserialization.") + s"Missing default parameter value for ignored field `${field.name}` on deserialization.") } + else if (field.embedded) format.fromMongoValue(bson) + else { + val value = bson.get(field.name) + if (value ne null) format.fromMongoValue(value.asInstanceOf[Any]) + else + defaultValue.getOrElse { + throw new Exception( + s"Missing required field '${field.name}' on deserialization.") + } + } } - } - val tuple = Tuple.fromArray(fields.toArray) - mirrorOfProduct.fromTuple(tuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) + val tuple = Tuple.fromArray(fields.toArray) + mirrorOfProduct.fromTuple(tuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) - case x => throw new Exception(s"BasicDBObject is expected for a class, instead got: $x") - } - ) - } + case x => throw new Exception(s"BasicDBObject is expected for a class, instead got: $x") + } + } inline private def summonFormatters[T <: Tuple]: Vector[MongoFormat[Any]] = inline erasedValue[T] match { @@ -143,17 +149,4 @@ object MongoFormat { } } - - // This is needed to remove the "New anonymous class definition will be duplicated at each inline site" warnings - private def create[A]( - toMongo: A => Any, - fromMongo: Any => A, - fields: Vector[String] = MongoFormat.emptyFields): MongoFormat[A] = - new MongoFormat[A] { - override def toMongoValue(a: A): Any = toMongo(a) - - override def fromMongoValue(mongoType: Any): A = fromMongo(mongoType) - - override val fieldNames: Vector[String] = fields - } } diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala index cc991d9d..7b5c5a91 100644 --- a/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala +++ b/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala @@ -28,10 +28,6 @@ case class TraitMetaData( subtypes: Map[String, CaseClassMetaData] ) { val typeDiscriminator: String = typeHintFieldRaw.map(_.value).getOrElse("type") - - val subTypeTypeHints: Map[String, String] = subtypes.collect { - case (name, classMeta) if classMeta.typeHint.isDefined => name -> classMeta.typeHint.get - } } object AnnotationReader { From 52e5e056e6a3d1c39dc2280a8d2f1f6b5db8c1f4 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Mon, 25 Nov 2024 13:08:35 +0100 Subject: [PATCH 046/142] Small refactor --- .../src/main/scala/io/sphere/mongo/format/MongoFormat.scala | 5 +---- .../scala/io/sphere/mongo/generic/AnnotationReader.scala | 5 +++++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/MongoFormat.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/MongoFormat.scala index ffcf9856..85b73db7 100644 --- a/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/MongoFormat.scala +++ b/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/MongoFormat.scala @@ -59,10 +59,7 @@ object MongoFormat { inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): MongoFormat[A] = new MongoFormat[A] { private val traitMetaData = AnnotationReader.readTraitMetaData[A] - private val typeHintMap = traitMetaData.subtypes.collect { - case (name, classMeta) if classMeta.typeHint.isDefined => - name -> classMeta.typeHint.get - } + private val typeHintMap = traitMetaData.subTypeTypeHints private val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) private val formatters = summonFormatters[mirrorOfSum.MirroredElemTypes] private val names = constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala index 7b5c5a91..bcf32d8f 100644 --- a/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala +++ b/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala @@ -28,6 +28,11 @@ case class TraitMetaData( subtypes: Map[String, CaseClassMetaData] ) { val typeDiscriminator: String = typeHintFieldRaw.map(_.value).getOrElse("type") + + val subTypeTypeHints: Map[String, String] = subtypes.collect { + case (name, classMeta) if classMeta.typeHint.isDefined => + name -> classMeta.typeHint.get + } } object AnnotationReader { From 4eb367622df66c64952556976b6bdbf18639d150 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Mon, 25 Nov 2024 13:11:37 +0100 Subject: [PATCH 047/142] Small refactor --- .../src/main/scala/io/sphere/mongo/format/MongoFormat.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/MongoFormat.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/MongoFormat.scala index 85b73db7..2b36dade 100644 --- a/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/MongoFormat.scala +++ b/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/MongoFormat.scala @@ -56,6 +56,7 @@ object MongoFormat { case p: Mirror.ProductOf[A] => deriveCaseClass(p) } + @annotation.nowarn("msg=New anonymous class definition will be duplicated at each inline site") inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): MongoFormat[A] = new MongoFormat[A] { private val traitMetaData = AnnotationReader.readTraitMetaData[A] @@ -87,6 +88,7 @@ object MongoFormat { } } + @annotation.nowarn("msg=New anonymous class definition will be duplicated at each inline site") inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): MongoFormat[A] = new MongoFormat[A] { private val caseClassMetaData = AnnotationReader.readCaseClassMetaData[A] From 830e49706e7fee1dbde4a479fef70ee1d0738b22 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 5 Dec 2024 17:40:11 +0100 Subject: [PATCH 048/142] Revert "Move sphere-json-derivation-3 to sphere-json-core (just the main, not the tests for now)" This reverts commit f2cbf08ae0b3cc43a591e40eddcde574283b52cf. --- build.sbt | 2 +- .../main/scala-3/io/sphere/json/JSON.scala | 147 ----------------- .../json/generic/AnnotationReader.scala | 153 ------------------ .../sphere/json/generic/JSONAnnotation.scala | 11 -- .../io/sphere/json/JSON.scala | 0 5 files changed, 1 insertion(+), 312 deletions(-) delete mode 100644 json/json-core/src/main/scala-3/io/sphere/json/JSON.scala delete mode 100644 json/json-core/src/main/scala-3/io/sphere/json/generic/AnnotationReader.scala delete mode 100644 json/json-core/src/main/scala-3/io/sphere/json/generic/JSONAnnotation.scala rename json/json-core/src/main/{scala-2 => scala}/io/sphere/json/JSON.scala (100%) diff --git a/build.sbt b/build.sbt index fffa2165..a35516a6 100644 --- a/build.sbt +++ b/build.sbt @@ -6,7 +6,7 @@ lazy val scala3 = "3.5.2" // sbt-github-actions needs configuration in `ThisBuild` ThisBuild / crossScalaVersions := Seq(scala2_12, scala2_13, scala3) -ThisBuild / scalaVersion := scala2_13 +ThisBuild / scalaVersion := scala3 ThisBuild / githubWorkflowPublishTargetBranches := List() ThisBuild / githubWorkflowJavaVersions := List(JavaSpec.temurin("17")) ThisBuild / githubWorkflowBuildPreamble ++= List( diff --git a/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala deleted file mode 100644 index e571ef8e..00000000 --- a/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala +++ /dev/null @@ -1,147 +0,0 @@ -package io.sphere.json - -import cats.data.Validated -import cats.implicits.* -import io.sphere.json.{JSON, JSONParseError, JValidation} -import io.sphere.json.generic.{AnnotationReader, CaseClassMetaData, Field, TraitMetaData} -import org.json4s.DefaultJsonFormats.given -import org.json4s.JsonAST.JValue -import org.json4s.{DefaultJsonFormats, JObject, JString, jvalue2monadic, jvalue2readerSyntax} - -import scala.deriving.Mirror - -trait JSON[A] extends FromJSON[A] with ToJSON[A] - -inline def deriveJSON[A](using Mirror.Of[A]): JSON[A] = JSON.derived - -object JSON extends JSONInstances with JSONLowPriorityImplicits { - private val emptyFieldsSet: Vector[String] = Vector.empty - - inline def apply[A: JSON]: JSON[A] = summon[JSON[A]] - inline given derived[A](using Mirror.Of[A]): JSON[A] = Derivation.derived[A] - - private def addField(jObject: JObject, field: Field, jValue: JValue): JValue = - jValue match { - case o: JObject => - if (field.embedded) JObject(jObject.obj ++ o.obj) - else JObject(jObject.obj :+ (field.fieldName -> o)) - case other => JObject(jObject.obj :+ (field.fieldName -> other)) - } - - private object Derivation { - - import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} - - inline def derived[A](using m: Mirror.Of[A]): JSON[A] = - inline m match { - case s: Mirror.SumOf[A] => deriveTrait(s) - case p: Mirror.ProductOf[A] => deriveCaseClass(p) - } - - inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): JSON[A] = - new JSON[A] { - private val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] - private val typeHintMap: Map[String, String] = traitMetaData.subtypes.collect { - case (name, classMeta) if classMeta.typeHint.isDefined => - name -> classMeta.typeHint.get - } - private val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) - private val jsons: Seq[JSON[Any]] = summonFormatters[mirrorOfSum.MirroredElemTypes] - private val names: Seq[String] = - constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector - .asInstanceOf[Vector[String]] - private val jsonsByNames: Map[String, JSON[Any]] = names.zip(jsons).toMap - - override def read(jValue: JValue): JValidation[A] = - jValue match { - case jObject: JObject => - val typeName = (jObject \ traitMetaData.typeDiscriminator).as[String] - val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - jsonsByNames(originalTypeName).read(jObject).map(_.asInstanceOf[A]) - case x => - Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$jValue'")) - } - - override def write(value: A): JValue = { - // we never get a trait here, only classes, it's safe to assume Product - val originalTypeName = value.asInstanceOf[Product].productPrefix - val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) - val json = jsonsByNames(originalTypeName).write(value).asInstanceOf[JObject] - val typeDiscriminator = traitMetaData.typeDiscriminator -> JString(typeName) - JObject(typeDiscriminator :: json.obj) - } - - } - - inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): JSON[A] = - new JSON[A] { - private val caseClassMetaData: CaseClassMetaData = AnnotationReader.readCaseClassMetaData[A] - private val jsons: Vector[JSON[Any]] = summonFormatters[mirrorOfProduct.MirroredElemTypes] - private val fieldsAndJsons: Vector[(Field, JSON[Any])] = caseClassMetaData.fields.zip(jsons) - - private val fieldNames: Vector[String] = fieldsAndJsons.flatMap { (field, json) => - if (field.embedded) json.fields.toVector :+ field.name - else Vector(field.name) - } - - override val fields: Set[String] = fieldNames.toSet - - override def write(value: A): JValue = { - val caseClassFields = value.asInstanceOf[Product].productIterator - jsons - .zip(caseClassFields) - .zip(caseClassMetaData.fields) - .foldLeft[JValue](JObject()) { case (jObject, ((json, fieldValue), field)) => - addField(jObject.asInstanceOf[JObject], field, json.write(fieldValue)) - } - } - - override def read(jValue: JValue): JValidation[A] = - jValue match { - case jObject: JObject => - for { - fieldsAsAList <- fieldsAndJsons - .map((field, format) => readField(field, format, jObject)) - .sequence - fieldsAsTuple = Tuple.fromArray(fieldsAsAList.toArray) - - } yield mirrorOfProduct.fromTuple( - fieldsAsTuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) - - case x => - Validated.invalidNel(JSONParseError(s"JSON object expected. $x")) - } - - private def readField(field: Field, json: JSON[Any], jObject: JObject): JValidation[Any] = - if (field.embedded) json.read(jObject) - else io.sphere.json.field(field.fieldName, field.defaultArgument)(jObject)(json) - - } - - inline private def summonFormatters[T <: Tuple]: Vector[JSON[Any]] = - inline erasedValue[T] match { - case _: EmptyTuple => Vector.empty - case _: (t *: ts) => - summonInline[JSON[t]] - .asInstanceOf[JSON[Any]] +: summonFormatters[ts] - } - } -} - -trait JSONLowPriorityImplicits { - implicit def fromJSONAndToJSON[A](implicit fromJSON: FromJSON[A], toJSON: ToJSON[A]): JSON[A] = - new JSON[A] { - override def read(jval: JValue): JValidation[A] = fromJSON.read(jval) - override def write(value: A): JValue = toJSON.write(value) - } -} - -class JSONException(msg: String) extends RuntimeException(msg) - -sealed abstract class JSONError -case class JSONFieldError(path: List[String], message: String) extends JSONError { - override def toString = path.mkString(" -> ") + ": " + message -} -case class JSONParseError(message: String) extends JSONError { - override def toString = message -} diff --git a/json/json-core/src/main/scala-3/io/sphere/json/generic/AnnotationReader.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/AnnotationReader.scala deleted file mode 100644 index 69c64576..00000000 --- a/json/json-core/src/main/scala-3/io/sphere/json/generic/AnnotationReader.scala +++ /dev/null @@ -1,153 +0,0 @@ -package io.sphere.json.generic - -import io.sphere.json.generic.JSONAnnotation -import io.sphere.json.generic.JSONTypeHint - -import scala.quoted.{Expr, Quotes, Type, Varargs} - -private type MA = JSONAnnotation - -case class Field( - name: String, - embedded: Boolean, - ignored: Boolean, - jsonKey: Option[JSONKey], - defaultArgument: Option[Any]) { - val fieldName: String = jsonKey.map(_.value).getOrElse(name) -} - -case class CaseClassMetaData( - name: String, - typeHintRaw: Option[JSONTypeHint], - fields: Vector[Field] -) { - val typeHint: Option[String] = - typeHintRaw.map(_.value).filterNot(_.toList.forall(_ == ' ')) -} - -case class TraitMetaData( - top: CaseClassMetaData, - typeHintFieldRaw: Option[JSONTypeHintField], - subtypes: Map[String, CaseClassMetaData] -) { - val typeDiscriminator: String = typeHintFieldRaw.map(_.value).getOrElse("type") -} - -class AnnotationReader(using q: Quotes) { - - import q.reflect.* - - def readCaseClassMetaData[T: Type]: Expr[CaseClassMetaData] = { - val sym = TypeRepr.of[T].typeSymbol - caseClassMetaData(sym) - } - - def readTraitMetaData[T: Type]: Expr[TraitMetaData] = { - val sym = TypeRepr.of[T].typeSymbol - val typeHintField = - sym.annotations.map(findJSONTypeHintField).find(_.isDefined).flatten match { - case Some(thf) => '{ Some($thf) } - case None => '{ None } - } - - '{ - TraitMetaData( - top = ${ caseClassMetaData(sym) }, - typeHintFieldRaw = $typeHintField, - subtypes = ${ subtypeAnnotations(sym) } - ) - } - } - - private def annotationTree(tree: Tree): Option[Expr[MA]] = - Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[MA]).map(_.asExprOf[MA]) - - private def findEmbedded(tree: Tree): Boolean = - Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[JSONEmbedded]).isDefined - - private def findIgnored(tree: Tree): Boolean = - Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[JSONIgnore]).isDefined - - private def findKey(tree: Tree): Option[Expr[JSONKey]] = - Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[JSONKey]).map(_.asExprOf[JSONKey]) - - private def findTypeHint(tree: Tree): Option[Expr[JSONTypeHint]] = - Option - .when(tree.isExpr)(tree.asExpr) - .filter(_.isExprOf[JSONTypeHint]) - .map(_.asExprOf[JSONTypeHint]) - - private def findJSONTypeHintField(tree: Tree): Option[Expr[JSONTypeHintField]] = - Option - .when(tree.isExpr)(tree.asExpr) - .filter(_.isExprOf[JSONTypeHintField]) - .map(_.asExprOf[JSONTypeHintField]) - - private def collectFieldInfo(companion: Symbol)(s: Symbol, paramIdx: Int): Expr[Field] = { - val embedded = Expr(s.annotations.exists(findEmbedded)) - val ignored = Expr(s.annotations.exists(findIgnored)) - val name = Expr(s.name) - val key = s.annotations.map(findKey).find(_.isDefined).flatten match { - case Some(k) => '{ Some($k) } - case None => '{ None } - } - val defArgOpt = companion - .methodMember(s"$$lessinit$$greater$$default$$${paramIdx + 1}") - .headOption - .map(dm => Ref(dm).asExprOf[Any]) match { - case Some(k) => '{ Some($k) } - case None => '{ None } - } - - '{ - Field( - name = $name, - embedded = $embedded, - ignored = $ignored, - jsonKey = $key, - defaultArgument = $defArgOpt) - } - } - - private def caseClassMetaData(sym: Symbol): Expr[CaseClassMetaData] = { - val caseParams = sym.primaryConstructor.paramSymss.take(1).flatten - val fields = Varargs(caseParams.zipWithIndex.map(collectFieldInfo(sym.companionModule))) - val name = Expr(sym.name) - val typeHint = sym.annotations.map(findTypeHint).find(_.isDefined).flatten match { - case Some(th) => '{ Some($th) } - case None => '{ None } - } - - '{ - CaseClassMetaData( - name = $name, - typeHintRaw = $typeHint, - fields = Vector($fields*) - ) - } - } - - private def subtypeAnnotation(sym: Symbol): Expr[(String, CaseClassMetaData)] = { - val name = Expr(sym.name) - val annots = caseClassMetaData(sym) - '{ ($name, $annots) } - } - - private def subtypeAnnotations(sym: Symbol): Expr[Map[String, CaseClassMetaData]] = { - val subtypes = Varargs(sym.children.map(subtypeAnnotation)) - '{ Map($subtypes*) } - } - -} - -object AnnotationReader { - inline def readCaseClassMetaData[T]: CaseClassMetaData = ${ readCaseClassMetaDataImpl[T] } - - inline def readTraitMetaData[T]: TraitMetaData = ${ readTraitMetaDataImpl[T] } - - private def readCaseClassMetaDataImpl[T: Type](using Quotes): Expr[CaseClassMetaData] = - AnnotationReader().readCaseClassMetaData[T] - - private def readTraitMetaDataImpl[T: Type](using Quotes): Expr[TraitMetaData] = - AnnotationReader().readTraitMetaData[T] -} diff --git a/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONAnnotation.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONAnnotation.scala deleted file mode 100644 index 7d3ace8d..00000000 --- a/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONAnnotation.scala +++ /dev/null @@ -1,11 +0,0 @@ -package io.sphere.json.generic - -import scala.annotation.StaticAnnotation - -sealed trait JSONAnnotation extends StaticAnnotation - -case class JSONEmbedded() extends JSONAnnotation -case class JSONIgnore() extends JSONAnnotation -case class JSONKey(value: String) extends JSONAnnotation -case class JSONTypeHintField(value: String) extends JSONAnnotation -case class JSONTypeHint(value: String) extends JSONAnnotation diff --git a/json/json-core/src/main/scala-2/io/sphere/json/JSON.scala b/json/json-core/src/main/scala/io/sphere/json/JSON.scala similarity index 100% rename from json/json-core/src/main/scala-2/io/sphere/json/JSON.scala rename to json/json-core/src/main/scala/io/sphere/json/JSON.scala From ab83fddfc4a131ff9ec7704ed06cc9ea6989930f Mon Sep 17 00:00:00 2001 From: Marcelo Gomes Date: Thu, 5 Dec 2024 17:30:53 +0100 Subject: [PATCH 049/142] [ENE-49] util-3 and json-3 pure Scala 3 modules --- .gitignore | 4 + build.sbt | 26 +- json/json-3/dependencies.sbt | 5 + .../main/scala/io/sphere/json/FromJSON.scala | 434 ++++++++++++ .../src/main/scala/io/sphere/json/JSON.scala | 30 + .../io/sphere/json/SphereJsonParser.scala | 19 + .../main/scala/io/sphere/json/ToJSON.scala | 229 ++++++ .../sphere/json/catsinstances/package.scala | 41 ++ .../json/generic/AnnotationReader.scala | 153 ++++ .../io/sphere/json/generic/Annotations.scala | 11 + .../io/sphere/json/generic/Derivation.scala | 126 ++++ .../sphere/json/generic/DeriveSingleton.scala | 82 +++ .../main/scala/io/sphere/json/package.scala | 116 +++ .../io/sphere/json/BigNumberParsingSpec.scala | 24 + .../io/sphere/json/DateTimeParsingSpec.scala | 173 +++++ .../sphere/json/DeriveSingletonJSONSpec.scala | 171 +++++ .../io/sphere/json/JSONEmbeddedSpec.scala | 131 ++++ .../scala/io/sphere/json/JSONProperties.scala | 170 +++++ .../test/scala/io/sphere/json/JSONSpec.scala | 410 +++++++++++ .../io/sphere/json/JodaJavaTimeCompat.scala | 68 ++ .../io/sphere/json/MoneyMarshallingSpec.scala | 110 +++ .../io/sphere/json/NullHandlingSpec.scala | 68 ++ .../io/sphere/json/OptionReaderSpec.scala | 150 ++++ .../io/sphere/json/SetHandlingSpec.scala | 17 + .../io/sphere/json/SphereJsonExample.scala | 46 ++ .../io/sphere/json/SphereJsonParserSpec.scala | 14 + .../scala/io/sphere/json/ToJSONSpec.scala | 34 + .../io/sphere/json/TypesSwitchSpec.scala | 87 +++ .../catsinstances/JSONCatsInstancesTest.scala | 53 ++ .../json/generic/DefaultValuesSpec.scala | 44 ++ .../io/sphere/json/generic/JSONKeySpec.scala | 47 ++ .../json/generic/JsonTypeHintFieldSpec.scala | 63 ++ util-3/dependencies.sbt | 7 + util-3/src/main/scala/Concurrent.scala | 13 + util-3/src/main/scala/LangTag.scala | 19 + util-3/src/main/scala/Logging.scala | 5 + util-3/src/main/scala/Memoizer.scala | 28 + util-3/src/main/scala/Money.scala | 666 ++++++++++++++++++ util-3/src/main/scala/Reflect.scala | 61 ++ util-3/src/main/scala/ValidatedFlatMap.scala | 23 + util-3/src/test/scala/DomainObjectsGen.scala | 25 + .../test/scala/HighPrecisionMoneySpec.scala | 218 ++++++ util-3/src/test/scala/LangTagSpec.scala | 27 + util-3/src/test/scala/MoneySpec.scala | 162 +++++ .../scala/ScalaLoggingCompatiblitySpec.scala | 18 + 45 files changed, 4425 insertions(+), 3 deletions(-) create mode 100644 json/json-3/dependencies.sbt create mode 100644 json/json-3/src/main/scala/io/sphere/json/FromJSON.scala create mode 100644 json/json-3/src/main/scala/io/sphere/json/JSON.scala create mode 100644 json/json-3/src/main/scala/io/sphere/json/SphereJsonParser.scala create mode 100644 json/json-3/src/main/scala/io/sphere/json/ToJSON.scala create mode 100644 json/json-3/src/main/scala/io/sphere/json/catsinstances/package.scala create mode 100644 json/json-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala create mode 100644 json/json-3/src/main/scala/io/sphere/json/generic/Annotations.scala create mode 100644 json/json-3/src/main/scala/io/sphere/json/generic/Derivation.scala create mode 100644 json/json-3/src/main/scala/io/sphere/json/generic/DeriveSingleton.scala create mode 100644 json/json-3/src/main/scala/io/sphere/json/package.scala create mode 100644 json/json-3/src/test/scala/io/sphere/json/BigNumberParsingSpec.scala create mode 100644 json/json-3/src/test/scala/io/sphere/json/DateTimeParsingSpec.scala create mode 100644 json/json-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala create mode 100644 json/json-3/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala create mode 100644 json/json-3/src/test/scala/io/sphere/json/JSONProperties.scala create mode 100644 json/json-3/src/test/scala/io/sphere/json/JSONSpec.scala create mode 100644 json/json-3/src/test/scala/io/sphere/json/JodaJavaTimeCompat.scala create mode 100644 json/json-3/src/test/scala/io/sphere/json/MoneyMarshallingSpec.scala create mode 100644 json/json-3/src/test/scala/io/sphere/json/NullHandlingSpec.scala create mode 100644 json/json-3/src/test/scala/io/sphere/json/OptionReaderSpec.scala create mode 100644 json/json-3/src/test/scala/io/sphere/json/SetHandlingSpec.scala create mode 100644 json/json-3/src/test/scala/io/sphere/json/SphereJsonExample.scala create mode 100644 json/json-3/src/test/scala/io/sphere/json/SphereJsonParserSpec.scala create mode 100644 json/json-3/src/test/scala/io/sphere/json/ToJSONSpec.scala create mode 100644 json/json-3/src/test/scala/io/sphere/json/TypesSwitchSpec.scala create mode 100644 json/json-3/src/test/scala/io/sphere/json/catsinstances/JSONCatsInstancesTest.scala create mode 100644 json/json-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala create mode 100644 json/json-3/src/test/scala/io/sphere/json/generic/JSONKeySpec.scala create mode 100644 json/json-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala create mode 100644 util-3/dependencies.sbt create mode 100644 util-3/src/main/scala/Concurrent.scala create mode 100644 util-3/src/main/scala/LangTag.scala create mode 100644 util-3/src/main/scala/Logging.scala create mode 100644 util-3/src/main/scala/Memoizer.scala create mode 100644 util-3/src/main/scala/Money.scala create mode 100644 util-3/src/main/scala/Reflect.scala create mode 100644 util-3/src/main/scala/ValidatedFlatMap.scala create mode 100644 util-3/src/test/scala/DomainObjectsGen.scala create mode 100644 util-3/src/test/scala/HighPrecisionMoneySpec.scala create mode 100644 util-3/src/test/scala/LangTagSpec.scala create mode 100644 util-3/src/test/scala/MoneySpec.scala create mode 100644 util-3/src/test/scala/ScalaLoggingCompatiblitySpec.scala diff --git a/.gitignore b/.gitignore index ad3def41..59c4d40b 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,7 @@ src_managed *.deb *.changes *.worksheet.sc +metals.sbt +.vscode +.metals +.bloop diff --git a/build.sbt b/build.sbt index a35516a6..60a9b686 100644 --- a/build.sbt +++ b/build.sbt @@ -6,7 +6,7 @@ lazy val scala3 = "3.5.2" // sbt-github-actions needs configuration in `ThisBuild` ThisBuild / crossScalaVersions := Seq(scala2_12, scala2_13, scala3) -ThisBuild / scalaVersion := scala3 +ThisBuild / scalaVersion := scala2_13 ThisBuild / githubWorkflowPublishTargetBranches := List() ThisBuild / githubWorkflowJavaVersions := List(JavaSpec.temurin("17")) ThisBuild / githubWorkflowBuildPreamble ++= List( @@ -77,6 +77,11 @@ lazy val `sphere-libs` = project publish := {} ) .aggregate( + // Scala 3 modules + `sphere-util-3`, + `sphere-json-3`, + + // Scala 2 modules `sphere-util`, `sphere-json`, `sphere-json-core`, @@ -88,6 +93,23 @@ lazy val `sphere-libs` = project `benchmarks` ) +// Scala 3 modules + +lazy val `sphere-util-3` = project + .in(file("./util-3")) + .settings(scalaVersion := scala3) + .settings(standardSettings: _*) + .settings(homepage := Some(url("https://github.com/commercetools/sphere-scala-libs/README.md"))) + +lazy val `sphere-json-3` = project + .in(file("./json/json-3")) + .settings(scalaVersion := scala3) + .settings(standardSettings: _*) + .settings(Fmpp.settings: _*) + .dependsOn(`sphere-util-3`) + +// Scala 2 modules + lazy val `sphere-util` = project .in(file("./util")) .settings(standardSettings: _*) @@ -147,8 +169,6 @@ lazy val `sphere-mongo` = project url("https://github.com/commercetools/sphere-scala-libs/blob/master/mongo/README.md"))) .dependsOn(`sphere-mongo-core`, `sphere-mongo-derivation`) -// benchmarks - lazy val benchmarks = project .settings(standardSettings: _*) .settings(publishArtifact := false, publish := {}) diff --git a/json/json-3/dependencies.sbt b/json/json-3/dependencies.sbt new file mode 100644 index 00000000..0dc28bc8 --- /dev/null +++ b/json/json-3/dependencies.sbt @@ -0,0 +1,5 @@ +libraryDependencies ++= Seq( + "org.json4s" %% "json4s-jackson" % "4.0.7", + "com.fasterxml.jackson.core" % "jackson-databind" % "2.17.2", + "org.typelevel" %% "cats-core" % "2.12.0" +) diff --git a/json/json-3/src/main/scala/io/sphere/json/FromJSON.scala b/json/json-3/src/main/scala/io/sphere/json/FromJSON.scala new file mode 100644 index 00000000..ed92252e --- /dev/null +++ b/json/json-3/src/main/scala/io/sphere/json/FromJSON.scala @@ -0,0 +1,434 @@ +package io.sphere.json + +import scala.util.control.NonFatal +import scala.collection.mutable.ListBuffer +import java.util.{Currency, Locale, UUID} + +import cats.data.NonEmptyList +import cats.data.Validated.{Invalid, Valid} +import cats.syntax.apply._ +import cats.syntax.traverse._ +import io.sphere.json.field +import io.sphere.util.{BaseMoney, HighPrecisionMoney, LangTag, Money} +import org.json4s.JsonAST._ +import org.joda.time.format.ISODateTimeFormat + +import scala.annotation.implicitNotFound +import java.time +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import org.joda.time.YearMonth +import org.joda.time.LocalTime +import org.joda.time.LocalDate + +/** Type class for types that can be read from JSON. */ +@implicitNotFound("Could not find an instance of FromJSON for ${A}") +trait FromJSON[@specialized A] extends Serializable { + def read(jval: JValue): JValidation[A] + final protected def fail(msg: String) = jsonParseError(msg) + + /** needed JSON fields - ignored if empty */ + val fields: Set[String] = FromJSON.emptyFieldsSet +} + +object FromJSON extends FromJSONInstances { + + private[FromJSON] val emptyFieldsSet: Set[String] = Set.empty + + @inline def apply[A](implicit instance: FromJSON[A]): FromJSON[A] = instance + + private val validNone = Valid(None) + private val validNil = Valid(Nil) + private val validEmptyAnyVector: Valid[Vector[Any]] = Valid(Vector.empty) + private def validList[A]: Valid[List[A]] = validNil + private def validEmptyVector[A]: Valid[Vector[A]] = + validEmptyAnyVector.asInstanceOf[Valid[Vector[A]]] + + implicit def optionMapReader[@specialized A](implicit + c: FromJSON[A]): FromJSON[Option[Map[String, A]]] = + new FromJSON[Option[Map[String, A]]] { + private val internalMapReader = mapReader[A] + + def read(jval: JValue): JValidation[Option[Map[String, A]]] = jval match { + case JNothing | JNull => validNone + case x => internalMapReader.read(x).map(Some.apply) + } + } + + implicit def optionReader[@specialized A](implicit c: FromJSON[A]): FromJSON[Option[A]] = + new FromJSON[Option[A]] { + def read(jval: JValue): JValidation[Option[A]] = jval match { + case JNothing | JNull | JObject(Nil) => validNone + case JObject(s) if fields.nonEmpty && s.forall(t => !fields.contains(t._1)) => + validNone // if none of the optional fields are in the JSON + case x => c.read(x).map(Option.apply) + } + override val fields: Set[String] = c.fields + } + + implicit def listReader[@specialized A](implicit r: FromJSON[A]): FromJSON[List[A]] = + new FromJSON[List[A]] { + + def read(jval: JValue): JValidation[List[A]] = jval match { + case JArray(l) => + if (l.isEmpty) validList[A] + else { + // "imperative" style for performances + val errors = new ListBuffer[JSONError]() + val valids = new ListBuffer[A]() + var failedOnce: Boolean = false + l.foreach { jval => + r.read(jval) match { + case Valid(s) if !failedOnce => + valids += s + case Invalid(nel) => + errors ++= nel.toList + failedOnce = true + case _ => () + } + } + if (errors.isEmpty) + Valid(valids.result()) + else + Invalid(NonEmptyList.fromListUnsafe(errors.result())) + } + case _ => fail("JSON Array expected.") + } + } + + implicit def seqReader[@specialized A](implicit r: FromJSON[A]): FromJSON[Seq[A]] = + new FromJSON[Seq[A]] { + def read(jval: JValue): JValidation[Seq[A]] = listReader(r).read(jval) + } + + implicit def setReader[@specialized A](implicit r: FromJSON[A]): FromJSON[Set[A]] = + new FromJSON[Set[A]] { + def read(jval: JValue): JValidation[Set[A]] = jval match { + case JArray(l) => + if (l.isEmpty) Valid(Set.empty) + else listReader(r).read(jval).map(Set.apply(_*)) + case _ => fail("JSON Array expected.") + } + } + + implicit def vectorReader[@specialized A](implicit r: FromJSON[A]): FromJSON[Vector[A]] = + new FromJSON[Vector[A]] { + import scala.collection.immutable.VectorBuilder + + def read(jval: JValue): JValidation[Vector[A]] = jval match { + case JArray(l) => + if (l.isEmpty) validEmptyVector + else { + // "imperative" style for performances + val errors = new ListBuffer[JSONError]() + val valids = new VectorBuilder[A]() + var failedOnce: Boolean = false + l.foreach { jval => + r.read(jval) match { + case Valid(s) if !failedOnce => + valids += s + case Invalid(nel) => + errors ++= nel.toList + failedOnce = true + case _ => () + } + } + if (errors.isEmpty) + Valid(valids.result()) + else + Invalid(NonEmptyList.fromListUnsafe(errors.result())) + } + case _ => fail("JSON Array expected.") + } + } + + implicit def nonEmptyListReader[A](implicit r: FromJSON[A]): FromJSON[NonEmptyList[A]] = + new FromJSON[NonEmptyList[A]] { + def read(jval: JValue): JValidation[NonEmptyList[A]] = + fromJValue[List[A]](jval).andThen { + case head :: tail => Valid(NonEmptyList(head, tail)) + case Nil => fail("Non-empty JSON array expected") + } + } + + implicit val intReader: FromJSON[Int] = new FromJSON[Int] { + def read(jval: JValue): JValidation[Int] = jval match { + case JInt(i) if i.isValidInt => Valid(i.toInt) + case JLong(i) if i.isValidInt => Valid(i.toInt) + case _ => fail("JSON Number in the range of an Int expected.") + } + } + + implicit val stringReader: FromJSON[String] = new FromJSON[String] { + def read(jval: JValue): JValidation[String] = jval match { + case JString(s) => Valid(s) + case _ => fail("JSON String expected.") + } + } + + implicit val bigIntReader: FromJSON[BigInt] = new FromJSON[BigInt] { + def read(jval: JValue): JValidation[BigInt] = jval match { + case JInt(i) => Valid(i) + case JLong(l) => Valid(l) + case _ => fail("JSON Number in the range of a BigInt expected.") + } + } + + implicit val shortReader: FromJSON[Short] = new FromJSON[Short] { + def read(jval: JValue): JValidation[Short] = jval match { + case JInt(i) if i.isValidShort => Valid(i.toShort) + case JLong(l) if l.isValidShort => Valid(l.toShort) + case _ => fail("JSON Number in the range of a Short expected.") + } + } + + implicit val longReader: FromJSON[Long] = new FromJSON[Long] { + def read(jval: JValue): JValidation[Long] = jval match { + case JInt(i) => Valid(i.toLong) + case JLong(l) => Valid(l) + case _ => fail("JSON Number in the range of a Long expected.") + } + } + + implicit val floatReader: FromJSON[Float] = new FromJSON[Float] { + def read(jval: JValue): JValidation[Float] = jval match { + case JDouble(d) => Valid(d.toFloat) + case _ => fail("JSON Number in the range of a Float expected.") + } + } + + implicit val doubleReader: FromJSON[Double] = new FromJSON[Double] { + def read(jval: JValue): JValidation[Double] = jval match { + case JDouble(d) => Valid(d) + case JInt(i) => Valid(i.toDouble) + case JLong(l) => Valid(l.toDouble) + case _ => fail("JSON Number in the range of a Double expected.") + } + } + + implicit val booleanReader: FromJSON[Boolean] = new FromJSON[Boolean] { + private val cachedTrue = Valid(true) + private val cachedFalse = Valid(false) + def read(jval: JValue): JValidation[Boolean] = jval match { + case JBool(b) => if (b) cachedTrue else cachedFalse + case _ => fail("JSON Boolean expected") + } + } + + implicit def mapReader[A: FromJSON]: FromJSON[Map[String, A]] = new FromJSON[Map[String, A]] { + def read(json: JValue): JValidation[Map[String, A]] = json match { + case JObject(fs) => + // Perf note: an imperative implementation does not seem faster + fs.traverse[JValidation, (String, A)] { f => + fromJValue[A](f._2).map(v => (f._1, v)) + }.map(_.toMap) + case _ => fail("JSON Object expected") + } + } + + implicit val moneyReader: FromJSON[Money] = new FromJSON[Money] { + import Money._ + + override val fields = Set(CentAmountField, CurrencyCodeField) + + def read(value: JValue): JValidation[Money] = value match { + case o: JObject => + (field[Long](CentAmountField)(o), field[Currency](CurrencyCodeField)(o)) match { + case (Valid(centAmount), Valid(currencyCode)) => + Valid(Money.fromCentAmount(centAmount, currencyCode)) + case (Invalid(e1), Invalid(e2)) => Invalid(e1.concat(e2.toList)) + case (e1 @ Invalid(_), _) => e1 + case (_, e2 @ Invalid(_)) => e2 + } + + case _ => fail("JSON object expected.") + } + } + + implicit val highPrecisionMoneyReader: FromJSON[HighPrecisionMoney] = + new FromJSON[HighPrecisionMoney] { + import HighPrecisionMoney._ + + override val fields = Set(PreciseAmountField, CurrencyCodeField, FractionDigitsField) + + def read(value: JValue): JValidation[HighPrecisionMoney] = value match { + case o: JObject => + val validatedFields = ( + field[Long](PreciseAmountField)(o), + field[Int](FractionDigitsField)(o), + field[Currency](CurrencyCodeField)(o), + field[Option[Long]](CentAmountField)(o)) + + validatedFields.tupled.andThen { + case (preciseAmount, fractionDigits, currencyCode, centAmount) => + HighPrecisionMoney + .fromPreciseAmount(preciseAmount, fractionDigits, currencyCode, centAmount) + .leftMap(_.map(JSONParseError.apply)) + } + + case _ => + fail("JSON object expected.") + } + } + + implicit val baseMoneyReader: FromJSON[BaseMoney] = new FromJSON[BaseMoney] { + def read(value: JValue): JValidation[BaseMoney] = value match { + case o: JObject => + field[Option[String]](BaseMoney.TypeField)(o).andThen { + case None => moneyReader.read(value) + case Some(Money.TypeName) => moneyReader.read(value) + case Some(HighPrecisionMoney.TypeName) => highPrecisionMoneyReader.read(value) + case Some(tpe) => + fail( + s"Unknown money type '$tpe'. Available types are: '${Money.TypeName}', '${HighPrecisionMoney.TypeName}'.") + } + + case _ => fail("JSON object expected.") + } + } + + implicit val currencyReader: FromJSON[Currency] = new FromJSON[Currency] { + val failMsg = "ISO 4217 code JSON String expected." + def failMsgFor(input: String) = s"Currency '$input' not valid as ISO 4217 code." + + private val cachedEUR = Valid(Currency.getInstance("EUR")) + private val cachedUSD = Valid(Currency.getInstance("USD")) + private val cachedGBP = Valid(Currency.getInstance("GBP")) + private val cachedJPY = Valid(Currency.getInstance("JPY")) + + def read(jval: JValue): JValidation[Currency] = jval match { + case JString(s) => + s match { + case "EUR" => cachedEUR + case "USD" => cachedUSD + case "GBP" => cachedGBP + case "JPY" => cachedJPY + case _ => + try Valid(Currency.getInstance(s)) + catch { + case _: IllegalArgumentException => fail(failMsgFor(s)) + } + } + case _ => fail(failMsg) + } + } + + implicit val jValueReader: FromJSON[JValue] = new FromJSON[JValue] { + def read(jval: JValue): JValidation[JValue] = Valid(jval) + } + + implicit val jObjectReader: FromJSON[JObject] = new FromJSON[JObject] { + def read(jval: JValue): JValidation[JObject] = jval match { + case o: JObject => Valid(o) + case _ => fail("JSON object expected") + } + } + + private val validUnit = Valid(()) + + implicit val unitReader: FromJSON[Unit] = new FromJSON[Unit] { + def read(jval: JValue): JValidation[Unit] = jval match { + case JNothing | JNull | JObject(Nil) => validUnit + case _ => fail("Unexpected JSON") + } + } + + private def jsonStringReader[T](errorMessageTemplate: String)( + fromString: String => T): FromJSON[T] = + new FromJSON[T] { + def read(jval: JValue): JValidation[T] = jval match { + case JString(s) => + try Valid(fromString(s)) + catch { + case NonFatal(_) => fail(errorMessageTemplate.format(s)) + } + case _ => fail("JSON string expected.") + } + } + + // Joda Time + implicit val dateTimeReader: FromJSON[DateTime] = { + val UTCDateTimeComponents = raw"(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\.(\d{3})Z".r + + jsonStringReader("Failed to parse date/time: %s") { + case UTCDateTimeComponents(year, month, days, hours, minutes, seconds, millis) => + new DateTime( + year.toInt, + month.toInt, + days.toInt, + hours.toInt, + minutes.toInt, + seconds.toInt, + millis.toInt, + DateTimeZone.UTC) + case otherwise => + new DateTime(otherwise, DateTimeZone.UTC) + } + } + + implicit val timeReader: FromJSON[LocalTime] = jsonStringReader("Failed to parse time: %s") { + ISODateTimeFormat.localTimeParser.parseDateTime(_).toLocalTime + } + + implicit val dateReader: FromJSON[LocalDate] = jsonStringReader("Failed to parse date: %s") { + ISODateTimeFormat.localDateParser.parseDateTime(_).toLocalDate + } + + implicit val yearMonthReader: FromJSON[YearMonth] = + jsonStringReader("Failed to parse year/month: %s") { + new YearMonth(_) + } + + // java.time + // this formatter is used to parse instant in an extra lenient way + // similar to what the joda `DateTime` constructor accepts + // the accepted grammar for joda is described here: https://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTimeParser-- + // this only supports the part where the date is specified + private val lenientInstantParser = + new time.format.DateTimeFormatterBuilder() + .appendPattern("uuuu[-MM[-dd]]") + .optionalStart() + .appendPattern("'T'[HH[:mm[:ss]]]") + .appendFraction(time.temporal.ChronoField.NANO_OF_SECOND, 0, 9, true) + .optionalStart() + .appendOffset("+HHmm", "Z") + .optionalEnd() + .optionalEnd() + .parseDefaulting(time.temporal.ChronoField.MONTH_OF_YEAR, 1L) + .parseDefaulting(time.temporal.ChronoField.DAY_OF_MONTH, 1L) + .parseDefaulting(time.temporal.ChronoField.HOUR_OF_DAY, 0L) + .parseDefaulting(time.temporal.ChronoField.MINUTE_OF_HOUR, 0L) + .parseDefaulting(time.temporal.ChronoField.SECOND_OF_MINUTE, 0L) + .parseDefaulting(time.temporal.ChronoField.NANO_OF_SECOND, 0L) + .parseDefaulting(time.temporal.ChronoField.OFFSET_SECONDS, 0L) + .toFormatter() + + implicit val javaInstantReader: FromJSON[time.Instant] = + jsonStringReader("Failed to parse date/time: %s")(s => + time.Instant.from(lenientInstantParser.parse(s))) + + implicit val javaLocalTimeReader: FromJSON[time.LocalTime] = + jsonStringReader("Failed to parse time: %s")( + time.LocalTime.parse(_, time.format.DateTimeFormatter.ISO_LOCAL_TIME)) + + implicit val javaLocalDateReader: FromJSON[time.LocalDate] = + jsonStringReader("Failed to parse date: %s")( + time.LocalDate.parse(_, time.format.DateTimeFormatter.ISO_LOCAL_DATE)) + + implicit val javaYearMonthReader: FromJSON[time.YearMonth] = + jsonStringReader("Failed to parse year/month: %s")( + time.YearMonth.parse(_, JavaYearMonthFormatter)) + + implicit val uuidReader: FromJSON[UUID] = jsonStringReader("Invalid UUID: '%s'")(UUID.fromString) + + implicit val localeReader: FromJSON[Locale] = new FromJSON[Locale] { + def read(jval: JValue): JValidation[Locale] = jval match { + case JString(s) => + s match { + case LangTag(langTag) => Valid(langTag) + case _ => fail(LangTag.invalidLangTagMessage(s)) + } + case _ => fail("JSON string expected.") + } + } +} diff --git a/json/json-3/src/main/scala/io/sphere/json/JSON.scala b/json/json-3/src/main/scala/io/sphere/json/JSON.scala new file mode 100644 index 00000000..3e2366a2 --- /dev/null +++ b/json/json-3/src/main/scala/io/sphere/json/JSON.scala @@ -0,0 +1,30 @@ +package io.sphere.json + +import org.json4s.JsonAST.JValue + +import scala.annotation.implicitNotFound + +@implicitNotFound("Could not find an instance of JSON for ${A}") +trait JSON[A] extends FromJSON[A] with ToJSON[A] + +object JSON extends JSONInstances with JSONLowPriorityImplicits { + @inline def apply[A](implicit instance: JSON[A]): JSON[A] = instance +} + +trait JSONLowPriorityImplicits { + implicit def fromJSONAndToJSON[A](implicit fromJSON: FromJSON[A], toJSON: ToJSON[A]): JSON[A] = + new JSON[A] { + override def read(jval: JValue): JValidation[A] = fromJSON.read(jval) + override def write(value: A): JValue = toJSON.write(value) + } +} + +class JSONException(msg: String) extends RuntimeException(msg) + +sealed abstract class JSONError +case class JSONFieldError(path: List[String], message: String) extends JSONError { + override def toString = path.mkString(" -> ") + ": " + message +} +case class JSONParseError(message: String) extends JSONError { + override def toString = message +} diff --git a/json/json-3/src/main/scala/io/sphere/json/SphereJsonParser.scala b/json/json-3/src/main/scala/io/sphere/json/SphereJsonParser.scala new file mode 100644 index 00000000..12714da6 --- /dev/null +++ b/json/json-3/src/main/scala/io/sphere/json/SphereJsonParser.scala @@ -0,0 +1,19 @@ +package io.sphere.json + +import com.fasterxml.jackson.databind.DeserializationFeature.{ + USE_BIG_DECIMAL_FOR_FLOATS, + USE_BIG_INTEGER_FOR_INTS +} +import com.fasterxml.jackson.databind.ObjectMapper +import org.json4s.jackson.{Json4sScalaModule, JsonMethods} + +// extends the default JsonMethods to configure a different default jackson parser +private object SphereJsonParser extends JsonMethods { + override val mapper: ObjectMapper = { + val m = new ObjectMapper() + m.registerModule(new Json4sScalaModule) + m.configure(USE_BIG_INTEGER_FOR_INTS, false) + m.configure(USE_BIG_DECIMAL_FOR_FLOATS, false) + m + } +} diff --git a/json/json-3/src/main/scala/io/sphere/json/ToJSON.scala b/json/json-3/src/main/scala/io/sphere/json/ToJSON.scala new file mode 100644 index 00000000..8cf778ea --- /dev/null +++ b/json/json-3/src/main/scala/io/sphere/json/ToJSON.scala @@ -0,0 +1,229 @@ +package io.sphere.json + +import cats.data.NonEmptyList +import java.util.{Currency, Locale, UUID} + +import io.sphere.util.{BaseMoney, HighPrecisionMoney, Money} +import org.json4s.JsonAST._ +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import org.joda.time.LocalTime +import org.joda.time.LocalDate +import org.joda.time.YearMonth +import org.joda.time.format.ISODateTimeFormat + +import scala.annotation.implicitNotFound +import java.time + +/** Type class for types that can be written to JSON. */ +@implicitNotFound("Could not find an instance of ToJSON for ${A}") +trait ToJSON[@specialized A] extends Serializable { + def write(value: A): JValue +} + +class JSONWriteException(msg: String) extends JSONException(msg) + +object ToJSON extends ToJSONInstances { + + private val emptyJArray = JArray(Nil) + private val emptyJObject = JObject(Nil) + + @inline def apply[A](implicit instance: ToJSON[A]): ToJSON[A] = instance + + /** construct an instance from a function + */ + def instance[T](toJson: T => JValue): ToJSON[T] = new ToJSON[T] { + override def write(value: T): JValue = toJson(value) + } + + implicit def optionWriter[@specialized A](implicit c: ToJSON[A]): ToJSON[Option[A]] = + new ToJSON[Option[A]] { + def write(opt: Option[A]): JValue = opt match { + case Some(a) => c.write(a) + case None => JNothing + } + } + + implicit def listWriter[@specialized A](implicit w: ToJSON[A]): ToJSON[List[A]] = + new ToJSON[List[A]] { + def write(l: List[A]): JValue = + if (l.isEmpty) emptyJArray + else JArray(l.map(w.write)) + } + + implicit def nonEmptyListWriter[A](implicit w: ToJSON[A]): ToJSON[NonEmptyList[A]] = + new ToJSON[NonEmptyList[A]] { + def write(l: NonEmptyList[A]): JValue = JArray(l.toList.map(w.write)) + } + + implicit def seqWriter[@specialized A](implicit w: ToJSON[A]): ToJSON[Seq[A]] = + new ToJSON[Seq[A]] { + def write(s: Seq[A]): JValue = + if (s.isEmpty) emptyJArray + else JArray(s.iterator.map(w.write).toList) + } + + implicit def setWriter[@specialized A](implicit w: ToJSON[A]): ToJSON[Set[A]] = + new ToJSON[Set[A]] { + def write(s: Set[A]): JValue = + if (s.isEmpty) emptyJArray + else JArray(s.iterator.map(w.write).toList) + } + + implicit def vectorWriter[@specialized A](implicit w: ToJSON[A]): ToJSON[Vector[A]] = + new ToJSON[Vector[A]] { + def write(v: Vector[A]): JValue = + if (v.isEmpty) emptyJArray + else JArray(v.iterator.map(w.write).toList) + } + + implicit val intWriter: ToJSON[Int] = new ToJSON[Int] { + def write(i: Int): JValue = JLong(i) + } + + implicit val stringWriter: ToJSON[String] = new ToJSON[String] { + def write(s: String): JValue = JString(s) + } + + implicit val bigIntWriter: ToJSON[BigInt] = new ToJSON[BigInt] { + def write(i: BigInt): JValue = JInt(i) + } + + implicit val shortWriter: ToJSON[Short] = new ToJSON[Short] { + def write(s: Short): JValue = JLong(s) + } + + implicit val longWriter: ToJSON[Long] = new ToJSON[Long] { + def write(l: Long): JValue = JLong(l) + } + + implicit val floatWriter: ToJSON[Float] = new ToJSON[Float] { + def write(f: Float): JValue = JDouble(f) + } + + implicit val doubleWriter: ToJSON[Double] = new ToJSON[Double] { + def write(d: Double): JValue = JDouble(d) + } + + implicit val booleanWriter: ToJSON[Boolean] = new ToJSON[Boolean] { + def write(b: Boolean): JValue = if (b) JBool.True else JBool.False + } + + implicit def mapWriter[A: ToJSON]: ToJSON[Map[String, A]] = new ToJSON[Map[String, A]] { + def write(m: Map[String, A]) = + if (m.isEmpty) emptyJObject + else + JObject(m.iterator.map { case (k, v) => + JField(k, toJValue(v)) + }.toList) + } + + implicit val moneyWriter: ToJSON[Money] = new ToJSON[Money] { + import Money._ + + def write(m: Money): JValue = JObject( + JField(BaseMoney.TypeField, toJValue(m.`type`)) :: + JField(CurrencyCodeField, toJValue(m.currency)) :: + JField(CentAmountField, toJValue(m.centAmount)) :: + JField(FractionDigitsField, toJValue(m.currency.getDefaultFractionDigits)) :: + Nil + ) + } + + implicit val highPrecisionMoneyWriter: ToJSON[HighPrecisionMoney] = + new ToJSON[HighPrecisionMoney] { + import HighPrecisionMoney._ + def write(m: HighPrecisionMoney): JValue = JObject( + JField(BaseMoney.TypeField, toJValue(m.`type`)) :: + JField(CurrencyCodeField, toJValue(m.currency)) :: + JField(CentAmountField, toJValue(m.centAmount)) :: + JField(PreciseAmountField, toJValue(m.preciseAmount)) :: + JField(FractionDigitsField, toJValue(m.fractionDigits)) :: + Nil + ) + } + + implicit val baseMoneyWriter: ToJSON[BaseMoney] = new ToJSON[BaseMoney] { + def write(m: BaseMoney): JValue = m match { + case m: Money => moneyWriter.write(m) + case m: HighPrecisionMoney => highPrecisionMoneyWriter.write(m) + } + } + + implicit val currencyWriter: ToJSON[Currency] = new ToJSON[Currency] { + def write(c: Currency): JValue = toJValue(c.getCurrencyCode) + } + + implicit val jValueWriter: ToJSON[JValue] = new ToJSON[JValue] { + def write(jval: JValue): JValue = jval + } + + implicit val jObjectWriter: ToJSON[JObject] = new ToJSON[JObject] { + def write(jObj: JObject): JValue = jObj + } + + implicit val unitWriter: ToJSON[Unit] = new ToJSON[Unit] { + def write(u: Unit): JValue = JNothing + } + + // Joda time + implicit val dateTimeWriter: ToJSON[DateTime] = new ToJSON[DateTime] { + def write(dt: DateTime): JValue = JString( + ISODateTimeFormat.dateTime.print(dt.withZone(DateTimeZone.UTC))) + } + + implicit val timeWriter: ToJSON[LocalTime] = new ToJSON[LocalTime] { + def write(lt: LocalTime): JValue = JString(ISODateTimeFormat.time.print(lt)) + } + + implicit val dateWriter: ToJSON[LocalDate] = new ToJSON[LocalDate] { + def write(ld: LocalDate): JValue = JString(ISODateTimeFormat.date.print(ld)) + } + + implicit val yearMonthWriter: ToJSON[YearMonth] = new ToJSON[YearMonth] { + def write(ym: YearMonth): JValue = JString(ISODateTimeFormat.yearMonth().print(ym)) + } + + // java.time + + // always format the milliseconds + private val javaInstantFormatter = new time.format.DateTimeFormatterBuilder() + .appendInstant(3) + .toFormatter() + implicit val javaInstantWriter: ToJSON[time.Instant] = new ToJSON[time.Instant] { + def write(value: time.Instant): JValue = JString( + javaInstantFormatter.format(time.OffsetDateTime.ofInstant(value, time.ZoneOffset.UTC))) + } + + // always format the milliseconds + private val javaLocalTimeFormatter = new time.format.DateTimeFormatterBuilder() + .appendPattern("HH:mm:ss.SSS") + .toFormatter() + implicit val javaTimeWriter: ToJSON[time.LocalTime] = new ToJSON[time.LocalTime] { + def write(value: time.LocalTime): JValue = JString(javaLocalTimeFormatter.format(value)) + } + + implicit val javaDateWriter: ToJSON[time.LocalDate] = new ToJSON[time.LocalDate] { + def write(value: time.LocalDate): JValue = JString( + time.format.DateTimeFormatter.ISO_LOCAL_DATE.format(value)) + } + + implicit val javaYearMonth: ToJSON[time.YearMonth] = new ToJSON[time.YearMonth] { + def write(value: time.YearMonth): JValue = JString(JavaYearMonthFormatter.format(value)) + } + + implicit val uuidWriter: ToJSON[UUID] = new ToJSON[UUID] { + def write(uuid: UUID): JValue = JString(uuid.toString) + } + + implicit val localeWriter: ToJSON[Locale] = new ToJSON[Locale] { + def write(locale: Locale): JValue = JString(locale.toLanguageTag) + } + + implicit def eitherWriter[A: ToJSON, B: ToJSON]: ToJSON[Either[A, B]] = new ToJSON[Either[A, B]] { + def write(e: Either[A, B]): JValue = e match { + case Left(l) => toJValue(l) + case Right(r) => toJValue(r) + } + } +} diff --git a/json/json-3/src/main/scala/io/sphere/json/catsinstances/package.scala b/json/json-3/src/main/scala/io/sphere/json/catsinstances/package.scala new file mode 100644 index 00000000..779fd45d --- /dev/null +++ b/json/json-3/src/main/scala/io/sphere/json/catsinstances/package.scala @@ -0,0 +1,41 @@ +package io.sphere.json + +import _root_.cats.{Contravariant, Functor, Invariant} +import org.json4s.JValue + +/** Cats instances for [[JSON]], [[FromJSON]] and [[ToJSON]] + */ +package object catsinstances extends JSONInstances with FromJSONInstances with ToJSONInstances + +trait JSONInstances { + implicit val catsInvariantForJSON: Invariant[JSON] = new JSONInvariant +} + +trait FromJSONInstances { + implicit val catsFunctorForFromJSON: Functor[FromJSON] = new FromJSONFunctor +} + +trait ToJSONInstances { + implicit val catsContravariantForToJSON: Contravariant[ToJSON] = new ToJSONContravariant +} + +class JSONInvariant extends Invariant[JSON] { + override def imap[A, B](fa: JSON[A])(f: A => B)(g: B => A): JSON[B] = new JSON[B] { + override def write(b: B): JValue = fa.write(g(b)) + override def read(jval: JValue): JValidation[B] = fa.read(jval).map(f) + override val fields: Set[String] = fa.fields + } +} + +class FromJSONFunctor extends Functor[FromJSON] { + override def map[A, B](fa: FromJSON[A])(f: A => B): FromJSON[B] = new FromJSON[B] { + override def read(jval: JValue): JValidation[B] = fa.read(jval).map(f) + override val fields: Set[String] = fa.fields + } +} + +class ToJSONContravariant extends Contravariant[ToJSON] { + override def contramap[A, B](fa: ToJSON[A])(f: B => A): ToJSON[B] = new ToJSON[B] { + override def write(b: B): JValue = fa.write(f(b)) + } +} diff --git a/json/json-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala b/json/json-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala new file mode 100644 index 00000000..69c64576 --- /dev/null +++ b/json/json-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala @@ -0,0 +1,153 @@ +package io.sphere.json.generic + +import io.sphere.json.generic.JSONAnnotation +import io.sphere.json.generic.JSONTypeHint + +import scala.quoted.{Expr, Quotes, Type, Varargs} + +private type MA = JSONAnnotation + +case class Field( + name: String, + embedded: Boolean, + ignored: Boolean, + jsonKey: Option[JSONKey], + defaultArgument: Option[Any]) { + val fieldName: String = jsonKey.map(_.value).getOrElse(name) +} + +case class CaseClassMetaData( + name: String, + typeHintRaw: Option[JSONTypeHint], + fields: Vector[Field] +) { + val typeHint: Option[String] = + typeHintRaw.map(_.value).filterNot(_.toList.forall(_ == ' ')) +} + +case class TraitMetaData( + top: CaseClassMetaData, + typeHintFieldRaw: Option[JSONTypeHintField], + subtypes: Map[String, CaseClassMetaData] +) { + val typeDiscriminator: String = typeHintFieldRaw.map(_.value).getOrElse("type") +} + +class AnnotationReader(using q: Quotes) { + + import q.reflect.* + + def readCaseClassMetaData[T: Type]: Expr[CaseClassMetaData] = { + val sym = TypeRepr.of[T].typeSymbol + caseClassMetaData(sym) + } + + def readTraitMetaData[T: Type]: Expr[TraitMetaData] = { + val sym = TypeRepr.of[T].typeSymbol + val typeHintField = + sym.annotations.map(findJSONTypeHintField).find(_.isDefined).flatten match { + case Some(thf) => '{ Some($thf) } + case None => '{ None } + } + + '{ + TraitMetaData( + top = ${ caseClassMetaData(sym) }, + typeHintFieldRaw = $typeHintField, + subtypes = ${ subtypeAnnotations(sym) } + ) + } + } + + private def annotationTree(tree: Tree): Option[Expr[MA]] = + Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[MA]).map(_.asExprOf[MA]) + + private def findEmbedded(tree: Tree): Boolean = + Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[JSONEmbedded]).isDefined + + private def findIgnored(tree: Tree): Boolean = + Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[JSONIgnore]).isDefined + + private def findKey(tree: Tree): Option[Expr[JSONKey]] = + Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[JSONKey]).map(_.asExprOf[JSONKey]) + + private def findTypeHint(tree: Tree): Option[Expr[JSONTypeHint]] = + Option + .when(tree.isExpr)(tree.asExpr) + .filter(_.isExprOf[JSONTypeHint]) + .map(_.asExprOf[JSONTypeHint]) + + private def findJSONTypeHintField(tree: Tree): Option[Expr[JSONTypeHintField]] = + Option + .when(tree.isExpr)(tree.asExpr) + .filter(_.isExprOf[JSONTypeHintField]) + .map(_.asExprOf[JSONTypeHintField]) + + private def collectFieldInfo(companion: Symbol)(s: Symbol, paramIdx: Int): Expr[Field] = { + val embedded = Expr(s.annotations.exists(findEmbedded)) + val ignored = Expr(s.annotations.exists(findIgnored)) + val name = Expr(s.name) + val key = s.annotations.map(findKey).find(_.isDefined).flatten match { + case Some(k) => '{ Some($k) } + case None => '{ None } + } + val defArgOpt = companion + .methodMember(s"$$lessinit$$greater$$default$$${paramIdx + 1}") + .headOption + .map(dm => Ref(dm).asExprOf[Any]) match { + case Some(k) => '{ Some($k) } + case None => '{ None } + } + + '{ + Field( + name = $name, + embedded = $embedded, + ignored = $ignored, + jsonKey = $key, + defaultArgument = $defArgOpt) + } + } + + private def caseClassMetaData(sym: Symbol): Expr[CaseClassMetaData] = { + val caseParams = sym.primaryConstructor.paramSymss.take(1).flatten + val fields = Varargs(caseParams.zipWithIndex.map(collectFieldInfo(sym.companionModule))) + val name = Expr(sym.name) + val typeHint = sym.annotations.map(findTypeHint).find(_.isDefined).flatten match { + case Some(th) => '{ Some($th) } + case None => '{ None } + } + + '{ + CaseClassMetaData( + name = $name, + typeHintRaw = $typeHint, + fields = Vector($fields*) + ) + } + } + + private def subtypeAnnotation(sym: Symbol): Expr[(String, CaseClassMetaData)] = { + val name = Expr(sym.name) + val annots = caseClassMetaData(sym) + '{ ($name, $annots) } + } + + private def subtypeAnnotations(sym: Symbol): Expr[Map[String, CaseClassMetaData]] = { + val subtypes = Varargs(sym.children.map(subtypeAnnotation)) + '{ Map($subtypes*) } + } + +} + +object AnnotationReader { + inline def readCaseClassMetaData[T]: CaseClassMetaData = ${ readCaseClassMetaDataImpl[T] } + + inline def readTraitMetaData[T]: TraitMetaData = ${ readTraitMetaDataImpl[T] } + + private def readCaseClassMetaDataImpl[T: Type](using Quotes): Expr[CaseClassMetaData] = + AnnotationReader().readCaseClassMetaData[T] + + private def readTraitMetaDataImpl[T: Type](using Quotes): Expr[TraitMetaData] = + AnnotationReader().readTraitMetaData[T] +} diff --git a/json/json-3/src/main/scala/io/sphere/json/generic/Annotations.scala b/json/json-3/src/main/scala/io/sphere/json/generic/Annotations.scala new file mode 100644 index 00000000..7d3ace8d --- /dev/null +++ b/json/json-3/src/main/scala/io/sphere/json/generic/Annotations.scala @@ -0,0 +1,11 @@ +package io.sphere.json.generic + +import scala.annotation.StaticAnnotation + +sealed trait JSONAnnotation extends StaticAnnotation + +case class JSONEmbedded() extends JSONAnnotation +case class JSONIgnore() extends JSONAnnotation +case class JSONKey(value: String) extends JSONAnnotation +case class JSONTypeHintField(value: String) extends JSONAnnotation +case class JSONTypeHint(value: String) extends JSONAnnotation diff --git a/json/json-3/src/main/scala/io/sphere/json/generic/Derivation.scala b/json/json-3/src/main/scala/io/sphere/json/generic/Derivation.scala new file mode 100644 index 00000000..67a26666 --- /dev/null +++ b/json/json-3/src/main/scala/io/sphere/json/generic/Derivation.scala @@ -0,0 +1,126 @@ +package io.sphere.json.generic + +import cats.data.Validated +import cats.implicits.* +import io.sphere.json.{JSON, JSONParseError, JValidation} +import org.json4s.DefaultJsonFormats.given +import org.json4s.JsonAST.JValue +import org.json4s.{DefaultJsonFormats, JObject, JString, jvalue2monadic, jvalue2readerSyntax} + +import scala.deriving.Mirror + +inline def deriveJSON[A](using Mirror.Of[A]): JSON[A] = JSON.derived + +object JSON { + private val emptyFieldsSet: Vector[String] = Vector.empty + + inline def apply[A: JSON]: JSON[A] = summon[JSON[A]] + inline given derived[A](using Mirror.Of[A]): JSON[A] = Derivation.derived[A] + + private def addField(jObject: JObject, field: Field, jValue: JValue): JValue = + jValue match { + case o: JObject => + if (field.embedded) JObject(jObject.obj ++ o.obj) + else JObject(jObject.obj :+ (field.fieldName -> o)) + case other => JObject(jObject.obj :+ (field.fieldName -> other)) + } + + private object Derivation { + + import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} + + inline def derived[A](using m: Mirror.Of[A]): JSON[A] = + inline m match { + case s: Mirror.SumOf[A] => deriveTrait(s) + case p: Mirror.ProductOf[A] => deriveCaseClass(p) + } + + inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): JSON[A] = + new JSON[A] { + private val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] + private val typeHintMap: Map[String, String] = traitMetaData.subtypes.collect { + case (name, classMeta) if classMeta.typeHint.isDefined => + name -> classMeta.typeHint.get + } + private val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) + private val jsons: Seq[JSON[Any]] = summonFormatters[mirrorOfSum.MirroredElemTypes] + private val names: Seq[String] = + constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector + .asInstanceOf[Vector[String]] + private val jsonsByNames: Map[String, JSON[Any]] = names.zip(jsons).toMap + + override def read(jValue: JValue): JValidation[A] = + jValue match { + case jObject: JObject => + val typeName = (jObject \ traitMetaData.typeDiscriminator).as[String] + val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) + jsonsByNames(originalTypeName).read(jObject).map(_.asInstanceOf[A]) + case x => + Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$jValue'")) + } + + override def write(value: A): JValue = { + // we never get a trait here, only classes, it's safe to assume Product + val originalTypeName = value.asInstanceOf[Product].productPrefix + val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) + val json = jsonsByNames(originalTypeName).write(value).asInstanceOf[JObject] + val typeDiscriminator = traitMetaData.typeDiscriminator -> JString(typeName) + JObject(typeDiscriminator :: json.obj) + } + + } + + inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): JSON[A] = + new JSON[A] { + private val caseClassMetaData: CaseClassMetaData = AnnotationReader.readCaseClassMetaData[A] + private val jsons: Vector[JSON[Any]] = summonFormatters[mirrorOfProduct.MirroredElemTypes] + private val fieldsAndJsons: Vector[(Field, JSON[Any])] = caseClassMetaData.fields.zip(jsons) + + private val fieldNames: Vector[String] = fieldsAndJsons.flatMap { (field, json) => + if (field.embedded) json.fields.toVector :+ field.name + else Vector(field.name) + } + + override val fields: Set[String] = fieldNames.toSet + + override def write(value: A): JValue = { + val caseClassFields = value.asInstanceOf[Product].productIterator + jsons + .zip(caseClassFields) + .zip(caseClassMetaData.fields) + .foldLeft[JValue](JObject()) { case (jObject, ((json, fieldValue), field)) => + addField(jObject.asInstanceOf[JObject], field, json.write(fieldValue)) + } + } + + override def read(jValue: JValue): JValidation[A] = + jValue match { + case jObject: JObject => + for { + fieldsAsAList <- fieldsAndJsons + .map((field, format) => readField(field, format, jObject)) + .sequence + fieldsAsTuple = Tuple.fromArray(fieldsAsAList.toArray) + + } yield mirrorOfProduct.fromTuple( + fieldsAsTuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) + + case x => + Validated.invalidNel(JSONParseError(s"JSON object expected. $x")) + } + + private def readField(field: Field, json: JSON[Any], jObject: JObject): JValidation[Any] = + if (field.embedded) json.read(jObject) + else io.sphere.json.field(field.fieldName, field.defaultArgument)(jObject)(json) + + } + + inline private def summonFormatters[T <: Tuple]: Vector[JSON[Any]] = + inline erasedValue[T] match { + case _: EmptyTuple => Vector.empty + case _: (t *: ts) => + summonInline[JSON[t]] + .asInstanceOf[JSON[Any]] +: summonFormatters[ts] + } + } +} diff --git a/json/json-3/src/main/scala/io/sphere/json/generic/DeriveSingleton.scala b/json/json-3/src/main/scala/io/sphere/json/generic/DeriveSingleton.scala new file mode 100644 index 00000000..2b65c923 --- /dev/null +++ b/json/json-3/src/main/scala/io/sphere/json/generic/DeriveSingleton.scala @@ -0,0 +1,82 @@ +package io.sphere.json.generic + +import cats.data.Validated +import io.sphere.json.{JSON, JSONParseError, JValidation} +import org.json4s.{JNull, JString, JValue} + +import scala.deriving.Mirror + +inline def deriveSingletonJSON[A](using Mirror.Of[A]): JSON[A] = DeriveSingleton.derived + +object DeriveSingleton { + + inline given derived[A](using Mirror.Of[A]): JSON[A] = Derivation.derived[A] + + private object Derivation { + + import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} + + inline def derived[A](using m: Mirror.Of[A]): JSON[A] = + inline m match { + case s: Mirror.SumOf[A] => deriveTrait(s) + case p: Mirror.ProductOf[A] => deriveObject(p) + } + + inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): JSON[A] = + new JSON[A] { + private val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] + private val typeHintMap: Map[String, String] = traitMetaData.subtypes.collect { + case (name, classMeta) if classMeta.typeHint.isDefined => + name -> classMeta.typeHint.get + } + private val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) + private val jsons: Seq[JSON[Any]] = summonFormatters[mirrorOfSum.MirroredElemTypes] + private val names: Seq[String] = + constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector + .asInstanceOf[Vector[String]] + private val jsonsByNames: Map[String, JSON[Any]] = names.zip(jsons).toMap + + override def read(jValue: JValue): JValidation[A] = + jValue match { + case JString(typeName) => + val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) + jsonsByNames.get(originalTypeName) match { + case Some(json) => + json.read(JNull).map(_.asInstanceOf[A]) + case None => + Validated.invalidNel(JSONParseError(s"'$typeName' is not a valid value")) + } + + case x => + Validated.invalidNel(JSONParseError(s"JSON string expected. Got >>> $jValue")) + } + + override def write(value: A): JValue = { + val originalTypeName = value.asInstanceOf[Product].productPrefix + val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) + JString(typeName) + } + + } + + inline private def deriveObject[A](mirrorOfProduct: Mirror.ProductOf[A]): JSON[A] = + new JSON[A] { + override def write(value: A): JValue = ??? // This is already taken care of in `deriveTrait` + + override def read(jValue: JValue): JValidation[A] = { + // Just create the object instance, no need to do anything else + val tuple = Tuple.fromArray(Array.empty[Any]) + val obj = mirrorOfProduct.fromTuple(tuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) + Validated.Valid(obj) + } + } + + inline private def summonFormatters[T <: Tuple]: Vector[JSON[Any]] = + inline erasedValue[T] match { + case _: EmptyTuple => Vector.empty + case _: (t *: ts) => + summonInline[JSON[t]] + .asInstanceOf[JSON[Any]] +: summonFormatters[ts] + } + } +} diff --git a/json/json-3/src/main/scala/io/sphere/json/package.scala b/json/json-3/src/main/scala/io/sphere/json/package.scala new file mode 100644 index 00000000..cc1d4101 --- /dev/null +++ b/json/json-3/src/main/scala/io/sphere/json/package.scala @@ -0,0 +1,116 @@ +package io.sphere + +import cats.data.Validated.{Invalid, Valid} +import cats.data.{NonEmptyList, ValidatedNel} +import com.fasterxml.jackson.core.JsonParseException +import com.fasterxml.jackson.core.exc.{InputCoercionException, StreamConstraintsException} +import com.fasterxml.jackson.databind.JsonMappingException +import io.sphere.util.Logging +import org.json4s.{DefaultFormats, JsonInput, StringInput} +import org.json4s.JsonAST._ +import org.json4s.ParserUtil.ParseException +import org.json4s.jackson.compactJson +import java.time.format.DateTimeFormatter + +/** Provides functions for reading & writing JSON, via type classes JSON/JSONR/JSONW. */ +package object json extends Logging { + + private[json] val JavaYearMonthFormatter = + DateTimeFormatter.ofPattern("uuuu-MM") + + implicit val liftJsonFormats: DefaultFormats = DefaultFormats + + type JValidation[A] = ValidatedNel[JSONError, A] + + def parseJsonUnsafe(json: JsonInput): JValue = + SphereJsonParser.parse(json, useBigDecimalForDouble = false, useBigIntForLong = false) + + def parseJSON(json: JsonInput): JValidation[JValue] = + try Valid(parseJsonUnsafe(json)) + catch { + case e: ParseException => jsonParseError(e.getMessage) + case e: JsonMappingException => jsonParseError(e.getOriginalMessage) + case e: JsonParseException => jsonParseError(e.getOriginalMessage) + case e: InputCoercionException => jsonParseError(e.getOriginalMessage) + case e: StreamConstraintsException => jsonParseError(e.getOriginalMessage) + } + + def parseJSON(json: String): JValidation[JValue] = + parseJSON(StringInput(json)) + + def jsonParseError[A](msg: String): Invalid[NonEmptyList[JSONError]] = + Invalid(NonEmptyList.one(JSONParseError(msg))) + + def fromJSON[A: FromJSON](json: JsonInput): JValidation[A] = + parseJSON(json).andThen(fromJValue[A]) + + def fromJSON[A: FromJSON](json: String): JValidation[A] = + parseJSON(json).andThen(fromJValue[A]) + + private val jNothingStr = "{}" + + def toJSON[A: ToJSON](a: A): String = toJValue(a) match { + case JNothing => jNothingStr + case jval => compactJson(jval) + } + + /** Parses a JSON string into a type A. Throws a [[JSONException]] on failure. + * + * @param json + * The JSON string to parse. + * @return + * An instance of type A. + */ + def getFromJSON[A: FromJSON](json: JsonInput): A = + getFromJValue[A](parseJsonUnsafe(json)) + + def getFromJSON[A: FromJSON](json: String): A = + getFromJSON(StringInput(json)) + + def fromJValue[A](jval: JValue)(implicit json: FromJSON[A]): JValidation[A] = + json.read(jval) + + def toJValue[A](a: A)(implicit json: ToJSON[A]): JValue = + json.write(a) + + def getFromJValue[A: FromJSON](jval: JValue): A = + fromJValue[A](jval) match { + case Valid(a) => a + case Invalid(errs) => throw new JSONException(errs.toList.mkString(", ")) + } + + /** Extracts a JSON value of type A from a named field of a JSON object. + * + * @param name + * The name of the field. + * @param jObject + * The JObject from which to extract the field. + * @return + * A success with a value of type A or a non-empty list of errors. + */ + def field[A]( + name: String, + default: Option[A] = None + )(jObject: JObject)(implicit jsonr: FromJSON[A]): JValidation[A] = { + val fields = jObject.obj + // Perf note: avoiding Some(f) with fields.indexWhere and then constant time access is not faster + fields.find(f => f._1 == name && f._2 != JNull && f._2 != JNothing) match { + case Some(f) => + jsonr + .read(f._2) + .leftMap(errs => + errs.map { + case JSONParseError(msg) => JSONFieldError(name :: Nil, msg) + case JSONFieldError(path, msg) => JSONFieldError(name :: path, msg) + }) + case None => + default + .map(Valid(_)) + .orElse( + jsonr.read(JNothing).fold(_ => None, x => Some(Valid(x))) + ) // orElse(jsonr.default) + .getOrElse( + Invalid(NonEmptyList.one(JSONFieldError(name :: Nil, "Missing required value")))) + } + } +} diff --git a/json/json-3/src/test/scala/io/sphere/json/BigNumberParsingSpec.scala b/json/json-3/src/test/scala/io/sphere/json/BigNumberParsingSpec.scala new file mode 100644 index 00000000..11c192fe --- /dev/null +++ b/json/json-3/src/test/scala/io/sphere/json/BigNumberParsingSpec.scala @@ -0,0 +1,24 @@ +package io.sphere.json + +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class BigNumberParsingSpec extends AnyWordSpec with Matchers { + import BigNumberParsingSpec._ + + "parsing a big number" should { + "not take much time when parsed as Double" in { + fromJSON[Double](bigNumberAsString).isValid should be(false) + } + "not take much time when parsed as Long" in { + fromJSON[Long](bigNumberAsString).isValid should be(false) + } + "not take much time when parsed as Int" in { + fromJSON[Int](bigNumberAsString).isValid should be(false) + } + } +} + +object BigNumberParsingSpec { + private val bigNumberAsString = "9" * 10000000 +} diff --git a/json/json-3/src/test/scala/io/sphere/json/DateTimeParsingSpec.scala b/json/json-3/src/test/scala/io/sphere/json/DateTimeParsingSpec.scala new file mode 100644 index 00000000..562e9c3b --- /dev/null +++ b/json/json-3/src/test/scala/io/sphere/json/DateTimeParsingSpec.scala @@ -0,0 +1,173 @@ +package io.sphere.json + +import org.json4s.JString +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec +import java.time.Instant +import cats.data.Validated.Valid + +class DateTimeParsingSpec extends AnyWordSpec with Matchers { + + import FromJSON.dateTimeReader + import FromJSON.javaInstantReader + def jsonDateStringWith( + year: String = "2035", + dayOfTheMonth: String = "23", + monthOfTheYear: String = "11", + hourOfTheDay: String = "13", + minuteOfTheHour: String = "45", + secondOfTheMinute: String = "34", + millis: String = "543"): JString = + JString( + s"$year-$monthOfTheYear-${dayOfTheMonth}T$hourOfTheDay:$minuteOfTheHour:$secondOfTheMinute.${millis}Z") + + val beValid = be(Symbol("valid")) + val outOfIntRange = "999999999999999" + + "parsing a DateTime" should { + + "reject strings with invalid year" in { + dateTimeReader.read(jsonDateStringWith(year = "999999999")) shouldNot beValid + } + + "reject strings with years that are out of range for integers" in { + dateTimeReader.read(jsonDateStringWith(year = outOfIntRange)) shouldNot beValid + } + + "reject strings that are out of range for other fields" in { + dateTimeReader.read( + jsonDateStringWith( + monthOfTheYear = outOfIntRange, + dayOfTheMonth = outOfIntRange, + hourOfTheDay = outOfIntRange, + minuteOfTheHour = outOfIntRange, + secondOfTheMinute = outOfIntRange, + millis = outOfIntRange + )) shouldNot beValid + } + + "reject strings with invalid days" in { + dateTimeReader.read(jsonDateStringWith(dayOfTheMonth = "59")) shouldNot beValid + } + + "reject strings with invalid months" in { + dateTimeReader.read(jsonDateStringWith(monthOfTheYear = "39")) shouldNot beValid + } + + "reject strings with invalid hours" in { + dateTimeReader.read(jsonDateStringWith(hourOfTheDay = "39")) shouldNot beValid + } + + "reject strings with invalid minutes" in { + dateTimeReader.read(jsonDateStringWith(minuteOfTheHour = "87")) shouldNot beValid + } + + "reject strings with invalid seconds" in { + dateTimeReader.read(jsonDateStringWith(secondOfTheMinute = "87")) shouldNot beValid + } + } + + "parsing an Instant" should { + + "reject strings with invalid year" in { + javaInstantReader.read(jsonDateStringWith(year = "999999999")) shouldNot beValid + } + + "reject strings with years that are out of range for integers" in { + javaInstantReader.read(jsonDateStringWith(year = outOfIntRange)) shouldNot beValid + } + + "reject strings that are out of range for other fields" in { + javaInstantReader.read( + jsonDateStringWith( + monthOfTheYear = outOfIntRange, + dayOfTheMonth = outOfIntRange, + hourOfTheDay = outOfIntRange, + minuteOfTheHour = outOfIntRange, + secondOfTheMinute = outOfIntRange, + millis = outOfIntRange + )) shouldNot beValid + } + + "reject strings with invalid days" in { + javaInstantReader.read(jsonDateStringWith(dayOfTheMonth = "59")) shouldNot beValid + } + + "reject strings with invalid months" in { + javaInstantReader.read(jsonDateStringWith(monthOfTheYear = "39")) shouldNot beValid + } + + "reject strings with invalid hours" in { + javaInstantReader.read(jsonDateStringWith(hourOfTheDay = "39")) shouldNot beValid + } + + "reject strings with invalid minutes" in { + javaInstantReader.read(jsonDateStringWith(minuteOfTheHour = "87")) shouldNot beValid + } + + "reject strings with invalid seconds" in { + javaInstantReader.read(jsonDateStringWith(secondOfTheMinute = "87")) shouldNot beValid + } + } + + // ported from https://github.com/JodaOrg/joda-time/blob/4a1402a47cab4636bf4c73d42a62bfa80c1535ca/src/test/java/org/joda/time/convert/TestStringConverter.java#L114-L156 + // ensures that we accept similar patterns as joda when parsing instants + "parsing a Java instant" should { + "accept a full instant with milliseconds and offset" in { + javaInstantReader.read(JString("2004-06-09T12:24:48.501+0800")) shouldBe Valid( + Instant.parse("2004-06-09T04:24:48.501Z")) + } + + "accept a year with offset" in { + javaInstantReader.read(JString("2004T+0800")) shouldBe Valid( + Instant.parse("2004-01-01T00:00:00+08:00")) + } + + "accept a year month with offset" in { + javaInstantReader.read(JString("2004-06T+0800")) shouldBe Valid( + Instant.parse("2004-06-01T00:00:00+08:00")) + } + + "accept a year month day with offset" in { + javaInstantReader.read(JString("2004-06-09T+0800")) shouldBe Valid( + Instant.parse("2004-06-09T00:00:00+08:00")) + } + + "accept a year month day with hour and offset" in { + javaInstantReader.read(JString("2004-06-09T12+0800")) shouldBe Valid( + Instant.parse("2004-06-09T04:00:00Z")) + } + + "accept a year month day with hour, minute, and offset" in { + javaInstantReader.read(JString("2004-06-09T12:24+0800")) shouldBe Valid( + Instant.parse("2004-06-09T04:24:00Z")) + } + + "accept a year month day with hour, minute, second, and offset" in { + javaInstantReader.read(JString("2004-06-09T12:24:48+0800")) shouldBe Valid( + Instant.parse("2004-06-09T04:24:48Z")) + } + + "accept a year month day with hour, fraction, and offset" in { + javaInstantReader.read(JString("2004-06-09T12.5+0800")) shouldBe Valid( + Instant.parse("2004-06-09T04:00:00.5Z")) + } + + "accept a year month day with hour, minute, fraction, and offset" in { + javaInstantReader.read(JString("2004-06-09T12:24.5+0800")) shouldBe Valid( + Instant.parse("2004-06-09T04:24:00.5Z")) + } + + "accept a year month day with hour, minute, second, fraction, and offset" in { + javaInstantReader.read(JString("2004-06-09T12:24:48.5+0800")) shouldBe Valid( + Instant.parse("2004-06-09T04:24:48.5Z")) + } + + "accept a year month day with hour, minute, second, fraction, but no offset" in { + javaInstantReader.read(JString("2004-06-09T12:24:48.501")) shouldBe Valid( + Instant.parse("2004-06-09T12:24:48.501Z")) + } + + } + +} diff --git a/json/json-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala b/json/json-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala new file mode 100644 index 00000000..dc334403 --- /dev/null +++ b/json/json-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala @@ -0,0 +1,171 @@ +package io.sphere.json + +import cats.data.Validated.Valid +import io.sphere.json.generic.* +import org.json4s.DefaultJsonFormats.given +import org.json4s.{DynamicJValueImplicits, JArray, JObject, JValue} +import org.json4s.JsonAST.{JField, JNothing} +import org.json4s.jackson.JsonMethods.{compact, render} +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class DeriveSingletonJSONSpec extends AnyWordSpec with Matchers { + "DeriveSingletonJSON" must { + "read normal singleton values" in { + val user = getFromJSON[UserWithPicture](""" + { + "userId": "foo-123", + "pictureSize": "Medium", + "pictureUrl": "http://exmple.com" + } + """) + + user must be(UserWithPicture("foo-123", Medium, "http://exmple.com")) + } + + "fail to read if singleton value is unknown" in { + a[JSONException] must be thrownBy getFromJSON[UserWithPicture](""" + { + "userId": "foo-123", + "pictureSize": "foo", + "pictureUrl": "http://exmple.com" + } + """) + } + + "write normal singleton values" in { + val userJson = toJValue(UserWithPicture("foo-123", Medium, "http://exmple.com")) + + val Valid(expectedJson) = parseJSON(""" + { + "userId": "foo-123", + "pictureSize": "Medium", + "pictureUrl": "http://exmple.com" + } + """): @unchecked + + filter(userJson) must be(expectedJson) + } + + "read custom singleton values" in { + val user = getFromJSON[UserWithPicture](""" + { + "userId": "foo-123", + "pictureSize": "bar", + "pictureUrl": "http://exmple.com" + } + """) + + user must be(UserWithPicture("foo-123", Custom, "http://exmple.com")) + } + + "write custom singleton values" in { + val userJson = toJValue(UserWithPicture("foo-123", Custom, "http://exmple.com")) + + val Valid(expectedJson) = parseJSON(""" + { + "userId": "foo-123", + "pictureSize": "bar", + "pictureUrl": "http://exmple.com" + } + """): @unchecked + + filter(userJson) must be(expectedJson) + } + + "write and consequently read, which must produce the original value" in { + val originalUser = UserWithPicture("foo-123", Medium, "http://exmple.com") + val newUser = getFromJSON[UserWithPicture](compact(render(toJValue(originalUser)))) + + newUser must be(originalUser) + } + + "read and write sealed trait with only one subtype" in { + val json = """ + { + "userId": "foo-123", + "pictureSize": "Medium", + "pictureUrl": "http://example.com", + "access": { + "type": "Authorized", + "project": "internal" + } + } + """ + val user = getFromJSON[UserWithPicture](json) + + user must be( + UserWithPicture( + "foo-123", + Medium, + "http://example.com", + Some(Access.Authorized("internal")))) + + val newJson = toJValue[UserWithPicture](user) + Valid(newJson) must be(parseJSON(json)) + + val Valid(newUser) = fromJValue[UserWithPicture](newJson): @unchecked + newUser must be(user) + } + } + + private def filter(jvalue: JValue): JValue = + jvalue.removeField { + case (_, JNothing) => true + case _ => false + } + + extension (jv: JValue) { + def removeField(p: JField => Boolean): JValue = jv.transform { case JObject(l) => + JObject(l.filterNot(p)) + } + + def transform(f: PartialFunction[JValue, JValue]): JValue = map { x => + f.applyOrElse[JValue, JValue](x, _ => x) + } + + def map(f: JValue => JValue): JValue = { + def rec(v: JValue): JValue = v match { + case JObject(l) => f(JObject(l.map { case (n, va) => (n, rec(va)) })) + case JArray(l) => f(JArray(l.map(rec))) + case x => f(x) + } + + rec(jv) + } + } +} + +sealed abstract class PictureSize(val weight: Int, val height: Int) + +case object Small extends PictureSize(100, 100) +case object Medium extends PictureSize(500, 450) +case object Big extends PictureSize(1024, 2048) + +@JSONTypeHint("bar") +case object Custom extends PictureSize(1, 2) + +object PictureSize { + import DeriveSingleton.derived + + given JSON[PictureSize] = deriveSingletonJSON +} + +sealed trait Access +object Access { + // only one sub-type + import JSON.derived + case class Authorized(project: String) extends Access + + given JSON[Access] = deriveJSON +} + +case class UserWithPicture( + userId: String, + pictureSize: PictureSize, + pictureUrl: String, + access: Option[Access] = None) + +object UserWithPicture { + given JSON[UserWithPicture] = deriveJSON[UserWithPicture] +} diff --git a/json/json-3/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala b/json/json-3/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala new file mode 100644 index 00000000..f5c7eba4 --- /dev/null +++ b/json/json-3/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala @@ -0,0 +1,131 @@ +package io.sphere.json + +import org.scalatest.OptionValues +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec +import io.sphere.json.generic._ + +object JSONEmbeddedSpec { + + case class Embedded(value1: String, value2: Int) + + object Embedded { + given JSON[Embedded] = deriveJSON[Embedded] + } + + case class Test1(name: String, @JSONEmbedded embedded: Embedded) + + object Test1 { + given JSON[Test1] = deriveJSON[Test1] + } + + case class Test2(name: String, @JSONEmbedded embedded: Option[Embedded] = None) + + object Test2 { + given JSON[Test2] = deriveJSON + } + + case class SubTest4(@JSONEmbedded embedded: Embedded) + object SubTest4 { + given JSON[SubTest4] = deriveJSON + } + + case class Test4(subField: Option[SubTest4] = None) + object Test4 { + given JSON[Test4] = deriveJSON + } +} + +class JSONEmbeddedSpec extends AnyWordSpec with Matchers with OptionValues { + import JSONEmbeddedSpec._ + + "JSONEmbedded" should { + "flatten the json in one object" in { + val json = + """{ + | "name": "ze name", + | "value1": "ze value1", + | "value2": 45 + |} + """.stripMargin + val test1 = getFromJSON[Test1](json) + test1.name mustEqual "ze name" + test1.embedded.value1 mustEqual "ze value1" + test1.embedded.value2 mustEqual 45 + + val result = toJSON(test1) + parseJSON(result) mustEqual parseJSON(json) + } + + "validate that the json contains all needed fields" in { + val json = + """{ + | "name": "ze name", + | "value1": "ze value1" + |} + """.stripMargin + fromJSON[Test1](json).isInvalid must be(true) + fromJSON[Test1]("""{"name": "a"}""").isInvalid must be(true) + } + + "support optional embedded attribute" in { + val json = + """{ + | "name": "ze name", + | "value1": "ze value1", + | "value2": 45 + |} + """.stripMargin + val test2 = getFromJSON[Test2](json) + test2.name mustEqual "ze name" + test2.embedded.value.value1 mustEqual "ze value1" + test2.embedded.value.value2 mustEqual 45 + + val result = toJSON(test2) + parseJSON(result) mustEqual parseJSON(json) + } + + "ignore unknown fields" in { + val json = + """{ + | "name": "ze name", + | "value1": "ze value1", + | "value2": 45, + | "value3": true + |} + """.stripMargin + val test2 = getFromJSON[Test2](json) + test2.name mustEqual "ze name" + test2.embedded.value.value1 mustEqual "ze value1" + test2.embedded.value.value2 mustEqual 45 + } + + "check for sub-fields" in { + val json = + """ + { + "subField": { + "value1": "ze value1", + "value2": 45 + } + } + """ + val test4 = getFromJSON[Test4](json) + test4.subField.value.embedded.value1 mustEqual "ze value1" + test4.subField.value.embedded.value2 mustEqual 45 + } + + "support the absence of optional embedded attribute" in { + val json = """{ "name": "ze name" }""" + val test2 = getFromJSON[Test2](json) + test2.name mustEqual "ze name" + test2.embedded mustEqual None + } + + "validate the absence of some embedded attributes" in { + val json = """{ "name": "ze name", "value1": "ze value1" }""" + fromJSON[Test2](json).isInvalid must be(true) + } + } + +} diff --git a/json/json-3/src/test/scala/io/sphere/json/JSONProperties.scala b/json/json-3/src/test/scala/io/sphere/json/JSONProperties.scala new file mode 100644 index 00000000..40ba1b5b --- /dev/null +++ b/json/json-3/src/test/scala/io/sphere/json/JSONProperties.scala @@ -0,0 +1,170 @@ +package io.sphere.json + +import scala.language.higherKinds +import io.sphere.util.Money +import java.util.{Currency, Locale, UUID} + +import cats.Eq +import cats.data.NonEmptyList +import cats.syntax.eq._ +import org.joda.time._ +import org.scalacheck._ +import java.time + +import scala.math.BigDecimal.RoundingMode + +object JSONProperties extends Properties("JSON") { + private def check[A: FromJSON: ToJSON: Eq](a: A): Boolean = { + val json = s"""[${toJSON(a)}]""" + val result = fromJSON[Seq[A]](json).toOption.map(_.head).get + val r = result === a + if (!r) println(s"result: $result - expected: $a") + r + } + + implicit def arbitraryVector[A: Arbitrary]: Arbitrary[Vector[A]] = + Arbitrary(Arbitrary.arbitrary[List[A]].map(_.toVector)) + + implicit def arbitraryNEL[A: Arbitrary]: Arbitrary[NonEmptyList[A]] = + Arbitrary(for { + a <- Arbitrary.arbitrary[A] + l <- Arbitrary.arbitrary[List[A]] + } yield NonEmptyList(a, l)) + + implicit def arbitraryCurrency: Arbitrary[Currency] = + Arbitrary(Gen + .oneOf(Currency.getInstance("EUR"), Currency.getInstance("USD"), Currency.getInstance("JPY"))) + + implicit def arbitraryLocale: Arbitrary[Locale] = { + // Filter because OS X thinks that 'C' and 'POSIX' are valid locales... + val locales = Locale.getAvailableLocales().filter(_.toLanguageTag() != "und") + Arbitrary(for { + i <- Gen.choose(0, locales.length - 1) + } yield locales(i)) + } + + implicit def arbitraryDateTime: Arbitrary[DateTime] = + Arbitrary(for { + y <- Gen.choose(-4000, 4000) + m <- Gen.choose(1, 12) + d <- Gen.choose(1, 28) + h <- Gen.choose(0, 23) + min <- Gen.choose(0, 59) + s <- Gen.choose(0, 59) + ms <- Gen.choose(0, 999) + } yield new DateTime(y, m, d, h, min, s, ms, DateTimeZone.UTC)) + + // generate dates between years -4000 and +4000 + implicit val javaInstant: Arbitrary[time.Instant] = + Arbitrary(Gen.choose(-188395027761000L, 64092207599999L).map(time.Instant.ofEpochMilli(_))) + + implicit val javaLocalTime: Arbitrary[time.LocalTime] = Arbitrary( + Gen.choose(0, 3600 * 24).map(time.LocalTime.ofSecondOfDay(_))) + + implicit def arbitraryDate: Arbitrary[LocalDate] = + Arbitrary(Arbitrary.arbitrary[DateTime].map(_.toLocalDate)) + + implicit def arbitraryTime: Arbitrary[LocalTime] = + Arbitrary(Arbitrary.arbitrary[DateTime].map(_.toLocalTime)) + + implicit def arbitraryYearMonth: Arbitrary[YearMonth] = + Arbitrary(Arbitrary.arbitrary[DateTime].map(dt => new YearMonth(dt.getYear, dt.getMonthOfYear))) + + implicit def arbitraryMoney: Arbitrary[Money] = + Arbitrary(for { + c <- Arbitrary.arbitrary[Currency] + i <- Arbitrary.arbitrary[Int] + } yield Money.fromDecimalAmount(i, c)(RoundingMode.HALF_EVEN)) + + implicit def arbitraryUUID: Arbitrary[UUID] = + Arbitrary(for { + most <- Arbitrary.arbitrary[Long] + least <- Arbitrary.arbitrary[Long] + } yield new UUID(most, least)) + + implicit val currencyEqual: Eq[Currency] = new Eq[Currency] { + def eqv(c1: Currency, c2: Currency) = c1.getCurrencyCode == c2.getCurrencyCode + } + implicit val localeEqual: Eq[Locale] = new Eq[Locale] { + def eqv(l1: Locale, l2: Locale) = l1.toLanguageTag == l2.toLanguageTag + } + implicit val moneyEqual: Eq[Money] = new Eq[Money] { + override def eqv(x: Money, y: Money): Boolean = x == y + } + implicit val dateTimeEqual: Eq[DateTime] = new Eq[DateTime] { + def eqv(dt1: DateTime, dt2: DateTime) = dt1 == dt2 + } + implicit val localTimeEqual: Eq[LocalTime] = new Eq[LocalTime] { + def eqv(dt1: LocalTime, dt2: LocalTime) = dt1 == dt2 + } + implicit val localDateEqual: Eq[LocalDate] = new Eq[LocalDate] { + def eqv(dt1: LocalDate, dt2: LocalDate) = dt1 == dt2 + } + implicit val yearMonthEqual: Eq[YearMonth] = new Eq[YearMonth] { + def eqv(dt1: YearMonth, dt2: YearMonth) = dt1 == dt2 + } + implicit val javaInstantEqual: Eq[time.Instant] = Eq.fromUniversalEquals + implicit val javaLocalDateEqual: Eq[time.LocalDate] = Eq.fromUniversalEquals + implicit val javaLocalTimeEqual: Eq[time.LocalTime] = Eq.fromUniversalEquals + implicit val javaYearMonthEqual: Eq[time.YearMonth] = Eq.fromUniversalEquals + + private def checkC[C[_]](name: String)(implicit + jri: FromJSON[C[Int]], + jwi: ToJSON[C[Int]], + arbi: Arbitrary[C[Int]], + eqi: Eq[C[Int]], + jrs: FromJSON[C[Short]], + jws: ToJSON[C[Short]], + arbs: Arbitrary[C[Short]], + eqs: Eq[C[Short]], + jrl: FromJSON[C[Long]], + jwl: ToJSON[C[Long]], + arbl: Arbitrary[C[Long]], + eql: Eq[C[Long]], + jrss: FromJSON[C[String]], + jwss: ToJSON[C[String]], + arbss: Arbitrary[C[String]], + eqss: Eq[C[String]], + jrf: FromJSON[C[Float]], + jwf: ToJSON[C[Float]], + arbf: Arbitrary[C[Float]], + eqf: Eq[C[Float]], + jrd: FromJSON[C[Double]], + jwd: ToJSON[C[Double]], + arbd: Arbitrary[C[Double]], + eqd: Eq[C[Double]], + jrb: FromJSON[C[Boolean]], + jwb: ToJSON[C[Boolean]], + arbb: Arbitrary[C[Boolean]], + eqb: Eq[C[Boolean]] + ) = { + property(s"read/write $name of Ints") = Prop.forAll((l: C[Int]) => check(l)) + property(s"read/write $name of Shorts") = Prop.forAll((l: C[Short]) => check(l)) + property(s"read/write $name of Longs") = Prop.forAll((l: C[Long]) => check(l)) + property(s"read/write $name of Strings") = Prop.forAll((l: C[String]) => check(l)) + property(s"read/write $name of Floats") = Prop.forAll((l: C[Float]) => check(l)) + property(s"read/write $name of Doubles") = Prop.forAll((l: C[Double]) => check(l)) + property(s"read/write $name of Booleans") = Prop.forAll((l: C[Boolean]) => check(l)) + } + + checkC[List]("List") + checkC[Vector]("Vector") + checkC[Set]("Set") + checkC[NonEmptyList]("NonEmptyList") + checkC[Option]("Option") + checkC[({ type l[v] = Map[String, v] })#l]("Map") + + property("read/write Unit") = Prop.forAll((u: Unit) => check(u)) + property("read/write Currency") = Prop.forAll((c: Currency) => check(c)) + property("read/write Money") = Prop.forAll((m: Money) => check(m)) + property("read/write Locale") = Prop.forAll((l: Locale) => check(l)) + property("read/write UUID") = Prop.forAll((u: UUID) => check(u)) + property("read/write DateTime") = Prop.forAll((u: DateTime) => check(u)) + property("read/write LocalDate") = Prop.forAll((u: LocalDate) => check(u)) + property("read/write LocalTime") = Prop.forAll((u: LocalTime) => check(u)) + property("read/write YearMonth") = Prop.forAll((u: YearMonth) => check(u)) + property("read/write java.time.Instant") = Prop.forAll((i: time.Instant) => check(i)) + property("read/write java.time.LocalDate") = Prop.forAll((d: time.LocalDate) => check(d)) + property("read/write java.time.LocalTime") = Prop.forAll((t: time.LocalTime) => check(t)) + property("read/write java.time.YearMonth") = Prop.forAll((ym: time.YearMonth) => check(ym)) +} diff --git a/json/json-3/src/test/scala/io/sphere/json/JSONSpec.scala b/json/json-3/src/test/scala/io/sphere/json/JSONSpec.scala new file mode 100644 index 00000000..d3c86330 --- /dev/null +++ b/json/json-3/src/test/scala/io/sphere/json/JSONSpec.scala @@ -0,0 +1,410 @@ +package io.sphere.json + +import cats.data.Validated.{Invalid, Valid} +import cats.data.ValidatedNel +import cats.syntax.apply.* +import org.json4s.JsonAST.* +import io.sphere.json.field +import io.sphere.json.generic.* +import io.sphere.util.Money +import org.joda.time.* +import org.scalatest.matchers.must.Matchers +import org.scalatest.funspec.AnyFunSpec +import org.json4s.DefaultJsonFormats.given + +object JSONSpec { + case class Test(a: String) + + case class Project(nr: Int, name: String, version: Int = 1, milestones: List[Milestone] = Nil) + case class Milestone(name: String, date: Option[DateTime] = None) + + sealed abstract class Animal + case class Dog(name: String) extends Animal + case class Cat(name: String) extends Animal + case class Bird(name: String) extends Animal + + sealed trait GenericBase[A] + case class GenericA[A](a: A) extends GenericBase[A] + case class GenericB[A](a: A) extends GenericBase[A] + + object Singleton + + sealed abstract class SingletonEnum + case object SingletonA extends SingletonEnum + case object SingletonB extends SingletonEnum + case object SingletonC extends SingletonEnum + + sealed trait Mixed + case object SingletonMixed extends Mixed + case class RecordMixed(i: Int) extends Mixed + + object ScalaEnum extends Enumeration { + val One, Two, Three = Value + } + + // JSON instances for recursive data types cannot be derived + case class Node(value: Option[List[Node]]) +} + +class JSONSpec extends AnyFunSpec with Matchers { + import JSONSpec._ + + describe("JSON.apply") { + it("must find possible JSON instance") { + implicit val testJson: JSON[Test] = new JSON[Test] { + override def read(jval: JValue): JValidation[Test] = ??? + override def write(value: Test): JValue = ??? + } + + JSON[Test] must be(testJson) + } + + it("must create instance from FromJSON and ToJSON") { + JSON[Int] + JSON[List[Double]] + JSON[Map[String, Int]] + } + } + + describe("JSON") { + it("must read/write a custom class using custom typeclass instances") { + import JSONSpec.{Milestone, Project} + + implicit object MilestoneJSON extends JSON[Milestone] { + def write(m: Milestone): JValue = JObject( + JField("name", JString(m.name)) :: + JField("date", toJValue(m.date)) :: Nil + ) + def read(j: JValue): ValidatedNel[JSONError, Milestone] = j match { + case o: JObject => + (field[String]("name")(o), field[Option[DateTime]]("date")(o)).mapN(Milestone.apply) + case _ => fail("JSON object expected.") + } + } + implicit object ProjectJSON extends JSON[Project] { + def write(p: Project): JValue = JObject( + JField("nr", JInt(p.nr)) :: + JField("name", JString(p.name)) :: + JField("version", JInt(p.version)) :: + JField("milestones", toJValue(p.milestones)) :: + Nil + ) + def read(jval: JValue): ValidatedNel[JSONError, Project] = jval match { + case o: JObject => + ( + field[Int]("nr")(o), + field[String]("name")(o), + field[Int]("version", Some(1))(o), + field[List[Milestone]]("milestones", Some(Nil))(o)).mapN(Project.apply) + case _ => fail("JSON object expected.") + } + } + + val proj = Project(42, "Linux") + fromJSON[Project](toJSON(proj)) must equal(Valid(proj)) + + // Now some invalid JSON to test the error accumulation + val wrongTypeJSON = """ + { + "nr":"1", + "name":23, + "version":1, + "milestones":[{"name":"Bravo", "date": "xxx"}] + } + """ + + val Invalid(errs) = fromJSON[Project](wrongTypeJSON): @unchecked + errs.toList must equal( + List( + JSONFieldError(List("nr"), "JSON Number in the range of an Int expected."), + JSONFieldError(List("name"), "JSON String expected."), + JSONFieldError(List("milestones", "date"), "Failed to parse date/time: xxx") + )) + + // Now without a version value and without a milestones list. Defaults should apply. + val noVersionJSON = """{"nr":1,"name":"Linux"}""" + fromJSON[Project](noVersionJSON) must equal(Valid(Project(1, "Linux"))) + } + + it("must fail reading wrong currency code.") { + val wrongMoney = """{"currencyCode":"WRONG","centAmount":1000}""" + fromJSON[Money](wrongMoney).isInvalid must be(true) + } + + it("must provide derived JSON instances for product types (case classes)") { + import JSONSpec.{Milestone, Project} + given JSON[Milestone] = deriveJSON[Milestone] + given JSON[Project] = deriveJSON[Project] + val proj = + Project(42, "Linux", 7, Milestone("1.0") :: Milestone("2.0") :: Milestone("3.0") :: Nil) + fromJSON[Project](toJSON(proj)) must equal(Valid(proj)) + } + + it("must handle empty String") { + val Invalid(err) = fromJSON[Int](""): @unchecked + err.toList.head mustBe a[JSONParseError] + } + + it("must provide user-friendly error by empty String") { + val Invalid(err) = fromJSON[Int](""): @unchecked + err.toList mustEqual List(JSONParseError("No content to map due to end-of-input")) + } + + it("must handle incorrect json") { + val Invalid(err) = fromJSON[Int]("""{"key: "value"}"""): @unchecked + err.toList.head mustBe a[JSONParseError] + } + + it("must provide user-friendly error by incorrect json") { + val Invalid(err) = fromJSON[Int]("""{"key: "value"}"""): @unchecked + err.toList mustEqual List(JSONParseError( + "Unexpected character ('v' (code 118)): was expecting a colon to separate field name and value")) + } + + it("must provide derived JSON instances for sum types") { + import io.sphere.json.generic.JSON.derived + given JSON[Animal] = deriveJSON + List(Bird("Peewee"), Dog("Hasso"), Cat("Felidae")).foreach { animal => + fromJSON[Animal](toJSON(animal)) must equal(Valid(animal)) + } + } + + it("must provide derived instances for product types with concrete type parameters") { + given JSON[GenericA[String]] = deriveJSON[GenericA[String]] + val a = GenericA("hello") + fromJSON[GenericA[String]](toJSON(a)) must equal(Valid(a)) + } + + it("must provide derived instances for product types with generic type parameters") { + implicit def aJSON[A: JSON]: JSON[GenericA[A]] = deriveJSON[GenericA[A]] + val a = GenericA("hello") + fromJSON[GenericA[String]](toJSON(a)) must equal(Valid(a)) + } + +// it("must provide derived instances for singleton objects") { +// import io.sphere.json.generic.JSON.derived +// implicit val singletonJSON: JSON[JSONSpec.Singleton.type] = deriveJSON[JSONSpec.Singleton.type] +// +// val json = s"""[${toJSON(Singleton)}]""" +// withClue(json) { +// fromJSON[Seq[Singleton.type]](json) must equal(Valid(Seq(Singleton))) +// } +// +// implicit val singleEnumJSON: JSON[SingletonEnum] = deriveJSON[SingletonEnum] +// List(SingletonA, SingletonB, SingletonC).foreach { s => +// fromJSON[SingletonEnum](toJSON(s)) must equal(Valid(s)) +// } +// } + + it("must provide derived instances for sum types with a mix of case class / object") { + import io.sphere.json.generic.JSON.derived + given JSON[Mixed] = deriveJSON + List(SingletonMixed, RecordMixed(1)).foreach { m => + fromJSON[Mixed](toJSON(m)) must equal(Valid(m)) + } + } +// +// it("must provide derived instances for scala.Enumeration") { +// import io.sphere.json.generic.JSON.derived +// implicit val scalaEnumJSON: JSON[JSONSpec.ScalaEnum.Value] = deriveJSON[ScalaEnum.Value] +// ScalaEnum.values.foreach { v => +// val json = s"""[${toJSON(v)}]""" +// withClue(json) { +// fromJSON[Seq[ScalaEnum.Value]](json) must equal(Valid(Seq(v))) +// } +// } +// } +// +// it("must handle subclasses correctly in `jsonTypeSwitch`") { +// implicit val jsonImpl = TestSubjectBase.json +// +// val testSubjects = List[TestSubjectBase]( +// TestSubjectConcrete1("testSubject1"), +// TestSubjectConcrete2("testSubject2"), +// TestSubjectConcrete3("testSubject3"), +// TestSubjectConcrete4("testSubject4") +// ) +// +// testSubjects.foreach { testSubject => +// val json = toJSON(testSubject) +// withClue(json) { +// fromJSON[TestSubjectBase](json) must equal(Valid(testSubject)) +// } +// } +// +// } +// +// } +// +// describe("ToJSON and FromJSON") { +// it("must provide derived JSON instances for sum types") { +// // ToJSON +// implicit val birdToJSON = deriveJSON[Bird].write(Bird.apply _) +// implicit val dogToJSON = deriveJSON[Dog].write(Dog.apply _) +// implicit val catToJSON = toJsonProduct(Cat.apply _) +// implicit val animalToJSON = toJsonTypeSwitch[Animal, Bird, Dog, Cat](Nil) +// // FromJSON +// implicit val birdFromJSON = fromJsonProduct(Bird.apply _) +// implicit val dogFromJSON = fromJsonProduct(Dog.apply _) +// implicit val catFromJSON = fromJsonProduct(Cat.apply _) +// implicit val animalFromJSON = fromJsonTypeSwitch[Animal, Bird, Dog, Cat](Nil) +// +// List(Bird("Peewee"), Dog("Hasso"), Cat("Felidae")).foreach { +// a: Animal => +// fromJSON[Animal](toJSON(a)) must equal(Valid(a)) +// } +// } +// +// it("must provide derived instances for product types with concrete type parameters") { +// implicit val aToJSON = toJsonProduct(GenericA.apply[String] _) +// implicit val aFromJSON = fromJsonProduct(GenericA.apply[String] _) +// val a = GenericA("hello") +// fromJSON[GenericA[String]](toJSON(a)) must equal(Valid(a)) +// } +// +// it("must provide derived instances for singleton objects") { +// implicit val toSingletonJSON = toJsonSingleton(Singleton) +// implicit val fromSingletonJSON = fromJsonSingleton(Singleton) +// val json = s"""[${toJSON(Singleton)}]""" +// withClue(json) { +// fromJSON[Seq[Singleton.type]](json) must equal(Valid(Seq(Singleton))) +// } +// +// // ToJSON +// implicit val toSingleAJSON = toJsonSingleton(SingletonA) +// implicit val toSingleBJSON = toJsonSingleton(SingletonB) +// implicit val toSingleCJSON = toJsonSingleton(SingletonC) +// implicit val toSingleEnumJSON = +// toJsonSingletonEnumSwitch[SingletonEnum, SingletonA.type, SingletonB.type, SingletonC.type]( +// Nil) +// // FromJSON +// implicit val fromSingleAJSON = fromJsonSingleton(SingletonA) +// implicit val fromSingleBJSON = fromJsonSingleton(SingletonB) +// implicit val fromSingleCJSON = fromJsonSingleton(SingletonC) +// implicit val fromSingleEnumJSON = fromJsonSingletonEnumSwitch[ +// SingletonEnum, +// SingletonA.type, +// SingletonB.type, +// SingletonC.type](Nil) +// +// List(SingletonA, SingletonB, SingletonC).foreach { +// s: SingletonEnum => +// fromJSON[SingletonEnum](toJSON(s)) must equal(Valid(s)) +// } +// } +// +// it("must provide derived instances for sum types with a mix of case class / object") { +// // ToJSON +// implicit val toSingleJSON = toJsonProduct0(SingletonMixed) +// implicit val toRecordJSON = toJsonProduct(RecordMixed.apply _) +// implicit val toMixedJSON = toJsonTypeSwitch[Mixed, SingletonMixed.type, RecordMixed](Nil) +// // FromJSON +// implicit val fromSingleJSON = fromJsonProduct0(SingletonMixed) +// implicit val fromRecordJSON = fromJsonProduct(RecordMixed.apply _) +// implicit val fromMixedJSON = fromJsonTypeSwitch[Mixed, SingletonMixed.type, RecordMixed](Nil) +// List(SingletonMixed, RecordMixed(1)).foreach { +// m: Mixed => +// fromJSON[Mixed](toJSON(m)) must equal(Valid(m)) +// } +// } +// +// it("must provide derived instances for scala.Enumeration") { +// implicit val toScalaEnumJSON = toJsonEnum(ScalaEnum) +// implicit val fromScalaEnumJSON = fromJsonEnum(ScalaEnum) +// ScalaEnum.values.foreach { v => +// val json = s"""[${toJSON(v)}]""" +// withClue(json) { +// fromJSON[Seq[ScalaEnum.Value]](json) must equal(Valid(Seq(v))) +// } +// } +// } +// +// it("must handle subclasses correctly in `jsonTypeSwitch`") { +// // ToJSON +// implicit val to1 = toJsonProduct(TestSubjectConcrete1.apply _) +// implicit val to2 = toJsonProduct(TestSubjectConcrete2.apply _) +// implicit val to3 = toJsonProduct(TestSubjectConcrete3.apply _) +// implicit val to4 = toJsonProduct(TestSubjectConcrete4.apply _) +// implicit val toA = +// toJsonTypeSwitch[TestSubjectCategoryA, TestSubjectConcrete1, TestSubjectConcrete2](Nil) +// implicit val toB = +// toJsonTypeSwitch[TestSubjectCategoryB, TestSubjectConcrete3, TestSubjectConcrete4](Nil) +// implicit val toBase = +// toJsonTypeSwitch[TestSubjectBase, TestSubjectCategoryA, TestSubjectCategoryB](Nil) +// +// // FromJSON +// implicit val from1 = fromJsonProduct(TestSubjectConcrete1.apply _) +// implicit val from2 = fromJsonProduct(TestSubjectConcrete2.apply _) +// implicit val from3 = fromJsonProduct(TestSubjectConcrete3.apply _) +// implicit val from4 = fromJsonProduct(TestSubjectConcrete4.apply _) +// implicit val fromA = +// fromJsonTypeSwitch[TestSubjectCategoryA, TestSubjectConcrete1, TestSubjectConcrete2](Nil) +// implicit val fromB = +// fromJsonTypeSwitch[TestSubjectCategoryB, TestSubjectConcrete3, TestSubjectConcrete4](Nil) +// implicit val fromBase = +// fromJsonTypeSwitch[TestSubjectBase, TestSubjectCategoryA, TestSubjectCategoryB](Nil) +// +// val testSubjects = List[TestSubjectBase]( +// TestSubjectConcrete1("testSubject1"), +// TestSubjectConcrete2("testSubject2"), +// TestSubjectConcrete3("testSubject3"), +// TestSubjectConcrete4("testSubject4") +// ) +// +// testSubjects.foreach { testSubject => +// val json = toJSON(testSubject) +// withClue(json) { +// fromJSON[TestSubjectBase](json) must equal(Valid(testSubject)) +// } +// } +// +// } +// +// it("must provide derived JSON instances for product types (case classes)") { +// import JSONSpec.{Milestone, Project} +// // ToJSON +// implicit val milestoneToJSON = toJsonProduct(Milestone.apply _) +// implicit val projectToJSON = toJsonProduct(Project.apply _) +// // FromJSON +// implicit val milestoneFromJSON = fromJsonProduct(Milestone.apply _) +// implicit val projectFromJSON = fromJsonProduct(Project.apply _) +// +// val proj = +// Project(42, "Linux", 7, Milestone("1.0") :: Milestone("2.0") :: Milestone("3.0") :: Nil) +// fromJSON[Project](toJSON(proj)) must equal(Valid(proj)) +// } + } +} + +abstract class TestSubjectBase + +sealed abstract class TestSubjectCategoryA extends TestSubjectBase +sealed abstract class TestSubjectCategoryB extends TestSubjectBase + +@JSONTypeHint("foo") +case class TestSubjectConcrete1(c1: String) extends TestSubjectCategoryA +case class TestSubjectConcrete2(c2: String) extends TestSubjectCategoryA + +case class TestSubjectConcrete3(c3: String) extends TestSubjectCategoryB +case class TestSubjectConcrete4(c4: String) extends TestSubjectCategoryB + +object TestSubjectCategoryA { + + import io.sphere.json.generic.JSON.derived + val json: JSON[TestSubjectCategoryA] = deriveJSON[TestSubjectCategoryA] +} + +object TestSubjectCategoryB { + + import io.sphere.json.generic.JSON.derived + val json: JSON[TestSubjectCategoryB] = deriveJSON[TestSubjectCategoryB] +} + +//object TestSubjectBase { +// val json: JSON[TestSubjectBase] = { +// implicit val jsonA = TestSubjectCategoryA.json +// implicit val jsonB = TestSubjectCategoryB.json +// +// jsonTypeSwitch[TestSubjectBase, TestSubjectCategoryA, TestSubjectCategoryB](Nil) +// } +//} diff --git a/json/json-3/src/test/scala/io/sphere/json/JodaJavaTimeCompat.scala b/json/json-3/src/test/scala/io/sphere/json/JodaJavaTimeCompat.scala new file mode 100644 index 00000000..21437cfb --- /dev/null +++ b/json/json-3/src/test/scala/io/sphere/json/JodaJavaTimeCompat.scala @@ -0,0 +1,68 @@ +package io.sphere.json + +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import org.joda.time.LocalDate +import org.joda.time.LocalTime +import org.joda.time.YearMonth +import org.scalacheck.Arbitrary +import org.scalacheck.Gen +import org.scalacheck.Prop +import org.scalacheck.Properties +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import java.time.{Instant => JInstant} +import java.time.{LocalDate => JLocalDate} +import java.time.{LocalTime => JLocalTime} +import java.time.{YearMonth => JYearMonth} +import cats.data.Validated + +class JodaJavaTimeCompat extends Properties("Joda - java.time compat") { + val epochMillis = Gen.choose(-188395027761000L, 64092207599999L) + + implicit def arbitraryDateTime: Arbitrary[DateTime] = + Arbitrary(epochMillis.map(new DateTime(_, DateTimeZone.UTC))) + + // generate dates between years -4000 and +4000 + implicit val javaInstant: Arbitrary[JInstant] = + Arbitrary(epochMillis.map(JInstant.ofEpochMilli(_))) + + implicit val javaLocalTime: Arbitrary[JLocalTime] = Arbitrary( + Gen.choose(0, 3600 * 24).map(JLocalTime.ofSecondOfDay(_))) + + property("compatibility between serialized Instant and DateTime") = Prop.forAll { + (instant: JInstant) => + val dateTime = new DateTime(instant.toEpochMilli(), DateTimeZone.UTC) + val serializedInstant = ToJSON[JInstant].write(instant) + val serializedDateTime = ToJSON[DateTime].write(dateTime) + serializedInstant == serializedDateTime + } + + property("compatibility between serialized java.time.LocalTime and org.joda.time.LocalTime") = + Prop.forAll { (javaTime: JLocalTime) => + val jodaTime = LocalTime.fromMillisOfDay(javaTime.toNanoOfDay() / 1000000) + val serializedJavaTime = ToJSON[JLocalTime].write(javaTime) + val serializedJodaTime = ToJSON[LocalTime].write(jodaTime) + serializedJavaTime == serializedJodaTime + } + + property("roundtrip from java.time.Instant") = Prop.forAll { (instant: JInstant) => + FromJSON[DateTime] + .read(ToJSON[JInstant].write(instant)) + .andThen { dateTime => + FromJSON[JInstant].read(ToJSON[DateTime].write(dateTime)) + } + .fold(_ => false, _ == instant) + } + + property("roundtrip from org.joda.time.DateTime") = Prop.forAll { (dateTime: DateTime) => + FromJSON[JInstant] + .read(ToJSON[DateTime].write(dateTime)) + .andThen { instant => + FromJSON[DateTime].read(ToJSON[JInstant].write(instant)) + } + .fold(_ => false, _ == dateTime) + } + +} diff --git a/json/json-3/src/test/scala/io/sphere/json/MoneyMarshallingSpec.scala b/json/json-3/src/test/scala/io/sphere/json/MoneyMarshallingSpec.scala new file mode 100644 index 00000000..048971d5 --- /dev/null +++ b/json/json-3/src/test/scala/io/sphere/json/MoneyMarshallingSpec.scala @@ -0,0 +1,110 @@ +package io.sphere.json + +import java.util.Currency + +import io.sphere.util.{BaseMoney, HighPrecisionMoney, Money} +import cats.data.Validated.Valid +import org.json4s.jackson.compactJson +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class MoneyMarshallingSpec extends AnyWordSpec with Matchers { + "money encoding/decoding" should { + "be symmetric" in { + val money = Money.EUR(34.56) + val jsonAst = toJValue(money) + val jsonAsString = compactJson(jsonAst) + val Valid(readAst) = parseJSON(jsonAsString): @unchecked + + jsonAst should equal(readAst) + } + + "decode with type info" in { + val json = + """ + { + "type" : "centPrecision", + "currencyCode" : "USD", + "centAmount" : 3298 + } + """ + + fromJSON[BaseMoney](json) should be(Valid(Money.USD(BigDecimal("32.98")))) + } + + "decode without type info" in { + val json = + """ + { + "currencyCode" : "USD", + "centAmount" : 3298 + } + """ + + fromJSON[BaseMoney](json) should be(Valid(Money.USD(BigDecimal("32.98")))) + } + } + + "High precision money encoding/decoding" should { + "be symmetric" in { + implicit val mode = BigDecimal.RoundingMode.HALF_EVEN + + val money = HighPrecisionMoney.fromDecimalAmount(34.123456, 6, Currency.getInstance("EUR")) + val jsonAst = toJValue(money) + val jsonAsString = compactJson(jsonAst) + val Valid(readAst) = parseJSON(jsonAsString): @unchecked + val Valid(decodedMoney) = fromJSON[HighPrecisionMoney](jsonAsString): @unchecked + val Valid(decodedBaseMoney) = fromJSON[BaseMoney](jsonAsString): @unchecked + + jsonAst should equal(readAst) + decodedMoney should equal(money) + decodedBaseMoney should equal(money) + } + + "decode with type info" in { + val json = + """ + { + "type": "highPrecision", + "currencyCode": "USD", + "preciseAmount": 42, + "fractionDigits": 4 + } + """ + + fromJSON[BaseMoney](json) should be( + Valid(HighPrecisionMoney.USD(BigDecimal("0.0042"), Some(4)))) + } + + "decode with centAmount" in { + val Valid(json) = parseJSON(""" + { + "type": "highPrecision", + "currencyCode": "USD", + "preciseAmount": 42, + "centAmount": 1, + "fractionDigits": 4 + } + """): @unchecked + + val Valid(parsed) = fromJValue[BaseMoney](json): @unchecked + + toJValue(parsed) should be(json) + } + + "validate data when decoded from JSON" in { + val json = + """ + { + "type": "highPrecision", + "currencyCode": "USD", + "preciseAmount": 42, + "fractionDigits": 1 + } + """ + + fromJSON[BaseMoney](json).isValid should be(false) + } + } + +} diff --git a/json/json-3/src/test/scala/io/sphere/json/NullHandlingSpec.scala b/json/json-3/src/test/scala/io/sphere/json/NullHandlingSpec.scala new file mode 100644 index 00000000..5450d9e2 --- /dev/null +++ b/json/json-3/src/test/scala/io/sphere/json/NullHandlingSpec.scala @@ -0,0 +1,68 @@ +package io.sphere.json + +import io.sphere.json.generic._ +import org.json4s.JsonAST.{JNothing, JObject} +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class NullHandlingSpec extends AnyWordSpec with Matchers { + "JSON deserialization" must { + "accept undefined fields and use default values for them" in { + val jeans = getFromJSON[Jeans]("{}") + + jeans must be(Jeans(None, None, Set.empty, "secret")) + } + + "accept null values and use default values for them" in { + val jeans = getFromJSON[Jeans](""" + { + "leftPocket": null, + "rightPocket": null, + "backPocket": null, + "hiddenPocket": null + } + """) + + jeans must be(Jeans(None, None, Set.empty, "secret")) + } + + "accept JNothing values and use default values for them" in { + val jeans = getFromJValue[Jeans]( + JObject( + "leftPocket" -> JNothing, + "rightPocket" -> JNothing, + "backPocket" -> JNothing, + "hiddenPocket" -> JNothing)) + + jeans must be(Jeans(None, None, Set.empty, "secret")) + } + + "accept not-null values and use them" in { + val jeans = getFromJSON[Jeans](""" + { + "leftPocket": "Axe", + "rightPocket": "Magic powder", + "backPocket": ["Magic wand", "Rusty sword"], + "hiddenPocket": "The potion of healing" + } + """) + + jeans must be( + Jeans( + Some("Axe"), + Some("Magic powder"), + Set("Magic wand", "Rusty sword"), + "The potion of healing")) + } + } +} + +case class Jeans( + leftPocket: Option[String] = None, + rightPocket: Option[String], + backPocket: Set[String] = Set.empty, + hiddenPocket: String = "secret") + +object Jeans { + given JSON[Jeans] = deriveJSON[Jeans] +} diff --git a/json/json-3/src/test/scala/io/sphere/json/OptionReaderSpec.scala b/json/json-3/src/test/scala/io/sphere/json/OptionReaderSpec.scala new file mode 100644 index 00000000..8461d962 --- /dev/null +++ b/json/json-3/src/test/scala/io/sphere/json/OptionReaderSpec.scala @@ -0,0 +1,150 @@ +package io.sphere.json + +import io.sphere.json.generic._ +import org.json4s.{JArray, JLong, JNothing, JObject, JString} +import org.scalatest.OptionValues +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec +import org.json4s.DefaultJsonFormats.given + +object OptionReaderSpec { + + case class SimpleClass(value1: String, value2: Int) + + object SimpleClass { + given JSON[SimpleClass] = deriveJSON[SimpleClass] + } + + case class ComplexClass(name: String, simpleClass: Option[SimpleClass]) + + object ComplexClass { + given JSON[ComplexClass] = deriveJSON[ComplexClass] + } + + case class MapClass(id: Long, map: Option[Map[String, String]]) + object MapClass { + given JSON[MapClass] = deriveJSON[MapClass] + } + + case class ListClass(id: Long, list: Option[List[String]]) + object ListClass { + given JSON[ListClass] = deriveJSON[ListClass] + } +} + +class OptionReaderSpec extends AnyWordSpec with Matchers with OptionValues { + import OptionReaderSpec._ + + "OptionReader" should { + "handle presence of all fields" in { + val json = + """{ + | "value1": "a", + | "value2": 45 + |} + """.stripMargin + val result = getFromJSON[Option[SimpleClass]](json) + result.value.value1 mustEqual "a" + result.value.value2 mustEqual 45 + } + + "handle presence of all fields mixed with ignored fields" in { + val json = + """{ + | "value1": "a", + | "value2": 45, + | "value3": "b" + |} + """.stripMargin + val result = getFromJSON[Option[SimpleClass]](json) + result.value.value1 mustEqual "a" + result.value.value2 mustEqual 45 + } + + "handle presence of not all the fields" in { + val json = """{ "value1": "a" }""" + fromJSON[Option[SimpleClass]](json).isInvalid must be(true) + } + + "handle absence of all fields" in { + val json = "{}" + val result = getFromJSON[Option[SimpleClass]](json) + result must be(None) + } + + "handle optional map" in { + getFromJValue[MapClass](JObject("id" -> JLong(1L))) mustEqual MapClass(1L, None) + + getFromJValue[MapClass](JObject("id" -> JLong(1L), "map" -> JObject())) mustEqual + MapClass(1L, Some(Map.empty)) + + getFromJValue[MapClass]( + JObject("id" -> JLong(1L), "map" -> JObject("a" -> JString("b")))) mustEqual + MapClass(1L, Some(Map("a" -> "b"))) + + toJValue[MapClass](MapClass(1L, None)) mustEqual + JObject("id" -> JLong(1L), "map" -> JNothing) + toJValue[MapClass](MapClass(1L, Some(Map()))) mustEqual + JObject("id" -> JLong(1L), "map" -> JObject()) + toJValue[MapClass](MapClass(1L, Some(Map("a" -> "b")))) mustEqual + JObject("id" -> JLong(1L), "map" -> JObject("a" -> JString("b"))) + } + + "handle optional list" in { + getFromJValue[ListClass]( + JObject("id" -> JLong(1L), "list" -> JArray(List(JString("hi"))))) mustEqual + ListClass(1L, Some(List("hi"))) + getFromJValue[ListClass](JObject("id" -> JLong(1L), "list" -> JArray(List.empty))) mustEqual + ListClass(1L, Some(List())) + getFromJValue[ListClass](JObject("id" -> JLong(1L))) mustEqual + ListClass(1L, None) + + toJValue(ListClass(1L, Some(List("hi")))) mustEqual JObject( + "id" -> JLong(1L), + "list" -> JArray(List(JString("hi")))) + toJValue(ListClass(1L, Some(List.empty))) mustEqual JObject( + "id" -> JLong(1L), + "list" -> JArray(List.empty)) + toJValue(ListClass(1L, None)) mustEqual JObject("id" -> JLong(1L), "list" -> JNothing) + } + + "handle absence of all fields mixed with ignored fields" in { + val json = """{ "value3": "a" }""" + val result = getFromJSON[Option[SimpleClass]](json) + result must be(None) + } + + "consider all fields if the data type does not impose any restriction" in { + val json = + """{ + | "key1": "value1", + | "key2": "value2" + |} + """.stripMargin + val expected = Map("key1" -> "value1", "key2" -> "value2") + val result = getFromJSON[Map[String, String]](json) + result mustEqual expected + + val maybeResult = getFromJSON[Option[Map[String, String]]](json) + maybeResult.value mustEqual expected + } + + "parse optional element" in { + val json = + """{ + | "name": "ze name", + | "simpleClass": { + | "value1": "value1", + | "value2": 42 + | } + |} + """.stripMargin + val result = getFromJSON[ComplexClass](json) + result.simpleClass.value.value1 mustEqual "value1" + result.simpleClass.value.value2 mustEqual 42 + + parseJSON(toJSON(result)) mustEqual parseJSON(json) + } + } + +} diff --git a/json/json-3/src/test/scala/io/sphere/json/SetHandlingSpec.scala b/json/json-3/src/test/scala/io/sphere/json/SetHandlingSpec.scala new file mode 100644 index 00000000..74a2d069 --- /dev/null +++ b/json/json-3/src/test/scala/io/sphere/json/SetHandlingSpec.scala @@ -0,0 +1,17 @@ +package io.sphere.json + +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class SetHandlingSpec extends AnyWordSpec with Matchers { + "JSON deserialization" must { + + "should accept same elements in array to create a set" in { + val jeans = getFromJSON[Set[String]](""" + ["mobile", "mobile"] + """) + + jeans must be(Set("mobile")) + } + } +} diff --git a/json/json-3/src/test/scala/io/sphere/json/SphereJsonExample.scala b/json/json-3/src/test/scala/io/sphere/json/SphereJsonExample.scala new file mode 100644 index 00000000..69cd62ac --- /dev/null +++ b/json/json-3/src/test/scala/io/sphere/json/SphereJsonExample.scala @@ -0,0 +1,46 @@ +package io.sphere.json + +import io.sphere.json._ +import org.json4s.{JObject, JValue} +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class SphereJsonExample extends AnyWordSpec with Matchers { + + case class User(name: String, age: Int, location: String) + + object User { + + // update https://github.com/commercetools/sphere-scala-libs/blob/master/json/README.md in case of changed + implicit val json: JSON[User] = new JSON[User] { + import cats.data.ValidatedNel + import cats.syntax.apply._ + + def read(jval: JValue): ValidatedNel[JSONError, User] = jval match { + case o: JObject => + (field[String]("name")(o), field[Int]("age")(o), field[String]("location")(o)) + .mapN(User.apply) + case _ => fail("JSON object expected.") + } + + def write(u: User): JValue = JObject( + List( + "name" -> toJValue(u.name), + "age" -> toJValue(u.age), + "location" -> toJValue(u.location) + )) + } + } + + "JSON[User]" should { + "serialize and deserialize an user" in { + val user = User("name", 23, "earth") + val json = toJSON(user) + parseJSON(json).isValid should be(true) + + val newUser = getFromJSON[User](json) + newUser should be(user) + } + } + +} diff --git a/json/json-3/src/test/scala/io/sphere/json/SphereJsonParserSpec.scala b/json/json-3/src/test/scala/io/sphere/json/SphereJsonParserSpec.scala new file mode 100644 index 00000000..024348fd --- /dev/null +++ b/json/json-3/src/test/scala/io/sphere/json/SphereJsonParserSpec.scala @@ -0,0 +1,14 @@ +package io.sphere.json + +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class SphereJsonParserSpec extends AnyWordSpec with Matchers { + "Object mapper" must { + + "accept strings with 20_000_000 bytes" in { + SphereJsonParser.mapper.getFactory.streamReadConstraints().getMaxStringLength must be( + 20000000) + } + } +} diff --git a/json/json-3/src/test/scala/io/sphere/json/ToJSONSpec.scala b/json/json-3/src/test/scala/io/sphere/json/ToJSONSpec.scala new file mode 100644 index 00000000..acaeea18 --- /dev/null +++ b/json/json-3/src/test/scala/io/sphere/json/ToJSONSpec.scala @@ -0,0 +1,34 @@ +package io.sphere.json + +import java.util.UUID + +import org.json4s._ +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class ToJSONSpec extends AnyWordSpec with Matchers { + + case class User(id: UUID, firstName: String, age: Int) + + "ToJSON.apply" must { + "create a ToJSON" in { + implicit val encodeUser: ToJSON[User] = ToJSON.instance[User](u => + JObject( + List( + "id" -> toJValue(u.id), + "first_name" -> toJValue(u.firstName), + "age" -> toJValue(u.age) + ))) + + val id = UUID.randomUUID() + val json = toJValue(User(id, "bidule", 109)) + json must be( + JObject( + List( + "id" -> JString(id.toString), + "first_name" -> JString("bidule"), + "age" -> JLong(109) + ))) + } + } +} diff --git a/json/json-3/src/test/scala/io/sphere/json/TypesSwitchSpec.scala b/json/json-3/src/test/scala/io/sphere/json/TypesSwitchSpec.scala new file mode 100644 index 00000000..88f49334 --- /dev/null +++ b/json/json-3/src/test/scala/io/sphere/json/TypesSwitchSpec.scala @@ -0,0 +1,87 @@ +//package io.sphere.json +// +//import io.sphere.json.generic.{TypeSelectorContainer, deriveJSON, jsonTypeSwitch} +//import org.json4s._ +//import org.scalatest.matchers.must.Matchers +//import org.scalatest.wordspec.AnyWordSpec +// +//class TypesSwitchSpec extends AnyWordSpec with Matchers { +// import TypesSwitchSpec._ +// +// "jsonTypeSwitch" must { +// "combine different sum types tree" in { +// val m: Seq[Message] = List( +// TypeA.ClassA1(23), +// TypeA.ClassA2("world"), +// TypeB.ClassB1(valid = false), +// TypeB.ClassB2(Seq("a23", "c62"))) +// +// val jsons = m.map(Message.json.write) +// jsons must be( +// List( +// JObject("number" -> JLong(23), "type" -> JString("ClassA1")), +// JObject("name" -> JString("world"), "type" -> JString("ClassA2")), +// JObject("valid" -> JBool(false), "type" -> JString("ClassB1")), +// JObject( +// "references" -> JArray(List(JString("a23"), JString("c62"))), +// "type" -> JString("ClassB2")) +// )) +// +// val messages = jsons.map(Message.json.read).map(_.toOption.get) +// messages must be(m) +// } +// } +// +// "TypeSelectorContainer" must { +// "have information about type value discriminators" in { +// val selectors = Message.json.typeSelectors +// selectors.map(_.typeValue) must contain.allOf( +// "ClassA1", +// "ClassA2", +// "TypeA", +// "ClassB1", +// "ClassB2", +// "TypeB") +// +// // I don't think it's useful to allow different type fields. How is it possible to deserialize one json +// // if different type fields are used? +// selectors.map(_.typeField) must be(List("type", "type", "type", "type", "type", "type")) +// +// selectors.map(_.clazz.getName) must contain.allOf( +// "io.sphere.json.TypesSwitchSpec$TypeA$ClassA1", +// "io.sphere.json.TypesSwitchSpec$TypeA$ClassA2", +// "io.sphere.json.TypesSwitchSpec$TypeA", +// "io.sphere.json.TypesSwitchSpec$TypeB$ClassB1", +// "io.sphere.json.TypesSwitchSpec$TypeB$ClassB2", +// "io.sphere.json.TypesSwitchSpec$TypeB" +// ) +// } +// } +// +//} +// +//object TypesSwitchSpec { +// +// trait Message +// object Message { +// // this can be dangerous is the same class name is used in both sum types +// // ex if we define TypeA.Class1 && TypeB.Class1 +// // as both will use the same type value discriminator +// implicit val json: JSON[Message] with TypeSelectorContainer = +// jsonTypeSwitch[Message, TypeA, TypeB](Nil) +// } +// +// sealed trait TypeA extends Message +// object TypeA { +// case class ClassA1(number: Int) extends TypeA +// case class ClassA2(name: String) extends TypeA +// implicit val json: JSON[TypeA] = deriveJSON[TypeA] +// } +// +// sealed trait TypeB extends Message +// object TypeB { +// case class ClassB1(valid: Boolean) extends TypeB +// case class ClassB2(references: Seq[String]) extends TypeB +// implicit val json: JSON[TypeB] = deriveJSON[TypeB] +// } +//} diff --git a/json/json-3/src/test/scala/io/sphere/json/catsinstances/JSONCatsInstancesTest.scala b/json/json-3/src/test/scala/io/sphere/json/catsinstances/JSONCatsInstancesTest.scala new file mode 100644 index 00000000..01c483fb --- /dev/null +++ b/json/json-3/src/test/scala/io/sphere/json/catsinstances/JSONCatsInstancesTest.scala @@ -0,0 +1,53 @@ +package io.sphere.json.catsinstances + +import cats.syntax.invariant._ +import cats.syntax.functor._ +import cats.syntax.contravariant._ +import io.sphere.json.JSON +import io.sphere.json._ +import org.json4s.JsonAST +import org.json4s.JsonAST.JString +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class JSONCatsInstancesTest extends AnyWordSpec with Matchers { + import JSONCatsInstancesTest._ + + "Invariant[JSON]" must { + "allow imaping a default format" in { + val myId = MyId("test") + val json = toJValue(myId) + json must be(JString("test")) + val myNewId = getFromJValue[MyId](json) + myNewId must be(myId) + } + } + + "Functor[FromJson] and Contramap[ToJson]" must { + "allow mapping and contramapping a default format" in { + val myId = MyId2("test") + val json = toJValue(myId) + json must be(JString("test")) + val myNewId = getFromJValue[MyId2](json) + myNewId must be(myId) + } + } +} + +object JSONCatsInstancesTest { + private val stringJson: JSON[String] = new JSON[String] { + override def write(value: String): JsonAST.JValue = ToJSON[String].write(value) + override def read(jval: JsonAST.JValue): JValidation[String] = FromJSON[String].read(jval) + } + + case class MyId(id: String) extends AnyVal + object MyId { + implicit val json: JSON[MyId] = stringJson.imap(MyId.apply)(_.id) + } + + case class MyId2(id: String) extends AnyVal + object MyId2 { + implicit val fromJson: FromJSON[MyId2] = FromJSON[String].map(apply) + implicit val toJson: ToJSON[MyId2] = ToJSON[String].contramap(_.id) + } +} diff --git a/json/json-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala b/json/json-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala new file mode 100644 index 00000000..7bca45fe --- /dev/null +++ b/json/json-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala @@ -0,0 +1,44 @@ +package io.sphere.json.generic + +import io.sphere.json._ +import io.sphere.json.generic._ +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class DefaultValuesSpec extends AnyWordSpec with Matchers { + import DefaultValuesSpec._ + + "deriving JSON" must { + "handle default values" in { + val json = "{ }" + val test = getFromJSON[Test](json) + test.value1 must be("hello") + test.value2 must be(None) + test.value3 must be(Some("hi")) + } + "handle Option with no explicit default values" in { + val json = "{ }" + val test2 = getFromJSON[Test2](json) + test2.value1 must be("hello") + test2.value2 must be(None) + } + } +} + +object DefaultValuesSpec { + case class Test( + value1: String = "hello", + value2: Option[String] = None, + value3: Option[String] = Some("hi") + ) + object Test { + given JSON[Test] = deriveJSON[Test] + } + case class Test2( + value1: String = "hello", + value2: Option[String] + ) + object Test2 { + given JSON[Test2] = deriveJSON[Test2] + } +} diff --git a/json/json-3/src/test/scala/io/sphere/json/generic/JSONKeySpec.scala b/json/json-3/src/test/scala/io/sphere/json/generic/JSONKeySpec.scala new file mode 100644 index 00000000..df2bb582 --- /dev/null +++ b/json/json-3/src/test/scala/io/sphere/json/generic/JSONKeySpec.scala @@ -0,0 +1,47 @@ +package io.sphere.json.generic + +import org.json4s.MonadicJValue.jvalueToMonadic +import org.json4s.jvalue2readerSyntax +import io.sphere.json._ +import io.sphere.json.generic._ +import org.json4s.DefaultReaders._ +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class JSONKeySpec extends AnyWordSpec with Matchers { + import JSONKeySpec._ + + "deriving JSON" must { + "rename fields annotated with @JSONKey" in { + val test = + Test(value1 = "value1", value2 = "value2", subTest = SubTest(value2 = "other_value2")) + + val json = toJValue(test) + (json \ "value1").as[Option[String]] must be(Some("value1")) + (json \ "value2").as[Option[String]] must be(None) + (json \ "new_value_2").as[Option[String]] must be(Some("value2")) + (json \ "new_sub_value_2").as[Option[String]] must be(Some("other_value2")) + + val newTest = getFromJValue[Test](json) + newTest must be(test) + } + } +} + +object JSONKeySpec { + case class SubTest( + @JSONKey("new_sub_value_2") value2: String + ) + object SubTest { + given JSON[SubTest] = deriveJSON[SubTest] + } + + case class Test( + value1: String, + @JSONKey("new_value_2") value2: String, + @JSONEmbedded subTest: SubTest + ) + object Test { + given JSON[Test] = deriveJSON[Test] + } +} diff --git a/json/json-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala b/json/json-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala new file mode 100644 index 00000000..4658d1d0 --- /dev/null +++ b/json/json-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala @@ -0,0 +1,63 @@ +package io.sphere.json.generic + +import cats.data.Validated.Valid +import io.sphere.json.* +import org.json4s.* +import org.scalatest.Inside +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class JsonTypeHintFieldSpec extends AnyWordSpec with Matchers with Inside { + import JsonTypeHintFieldSpec._ + + "JSONTypeHintField" must { + "allow to set another field to distinguish between types (toJValue)" in { + val user = UserWithPicture("foo-123", Medium, "http://example.com") + val expected = JObject( + List( + "userId" -> JString("foo-123"), + "pictureSize" -> JObject(List("pictureType" -> JString("Medium"))), + "pictureUrl" -> JString("http://example.com"))) + + val json = toJValue[UserWithPicture](user) + json must be(expected) + + inside(fromJValue[UserWithPicture](json)) { case Valid(parsedUser) => + parsedUser must be(user) + } + } + + "allow to set another field to distinguish between types (fromJSON)" in { + val json = + """ + { + "userId": "foo-123", + "pictureSize": { "pictureType": "Medium" }, + "pictureUrl": "http://example.com" + } + """ + + val Valid(user) = fromJSON[UserWithPicture](json): @unchecked + + user must be(UserWithPicture("foo-123", Medium, "http://example.com")) + } + } + +} + +object JsonTypeHintFieldSpec { + + @JSONTypeHintField(value = "pictureType") + sealed trait PictureSize + case object Small extends PictureSize + case object Medium extends PictureSize + case object Big extends PictureSize + + case class UserWithPicture(userId: String, pictureSize: PictureSize, pictureUrl: String) + + object UserWithPicture { + import io.sphere.json.generic.JSON.given + import io.sphere.json.generic.deriveJSON + given JSON[UserWithPicture] = deriveJSON[UserWithPicture] + } +} diff --git a/util-3/dependencies.sbt b/util-3/dependencies.sbt new file mode 100644 index 00000000..cb4ce29b --- /dev/null +++ b/util-3/dependencies.sbt @@ -0,0 +1,7 @@ +libraryDependencies ++= Seq( + "com.typesafe.scala-logging" %% "scala-logging" % "3.9.4", + "joda-time" % "joda-time" % "2.12.7", + "org.joda" % "joda-convert" % "2.2.3", + ("org.typelevel" % "cats-core" % "2.12.0").cross(CrossVersion.binary), + "org.json4s" %% "json4s-scalap" % "4.0.7" +) diff --git a/util-3/src/main/scala/Concurrent.scala b/util-3/src/main/scala/Concurrent.scala new file mode 100644 index 00000000..4dcbf7ca --- /dev/null +++ b/util-3/src/main/scala/Concurrent.scala @@ -0,0 +1,13 @@ +package io.sphere.util + +import java.util.concurrent.ThreadFactory +import java.util.concurrent.atomic.AtomicInteger + +object Concurrent { + def namedThreadFactory(poolName: String): ThreadFactory = + new ThreadFactory { + val count = new AtomicInteger(0) + override def newThread(r: Runnable) = + new Thread(r, poolName + "-" + count.incrementAndGet) + } +} diff --git a/util-3/src/main/scala/LangTag.scala b/util-3/src/main/scala/LangTag.scala new file mode 100644 index 00000000..0005c9f3 --- /dev/null +++ b/util-3/src/main/scala/LangTag.scala @@ -0,0 +1,19 @@ +package io.sphere.util + +import java.util.Locale + +/** Extractor for Locales, e.g. for use in pattern-matching request paths. */ +object LangTag { + + final val UNDEFINED: String = "und" + + class LocaleOpt(val locale: Locale) extends AnyVal { + // if toLanguageTag returns "und", it means the language tag is undefined + def isEmpty: Boolean = UNDEFINED == locale.toLanguageTag + def get: Locale = locale + } + + def unapply(s: String): LocaleOpt = new LocaleOpt(Locale.forLanguageTag(s)) + + def invalidLangTagMessage(invalidLangTag: String) = s"Invalid language tag: '$invalidLangTag'" +} diff --git a/util-3/src/main/scala/Logging.scala b/util-3/src/main/scala/Logging.scala new file mode 100644 index 00000000..e5035c4e --- /dev/null +++ b/util-3/src/main/scala/Logging.scala @@ -0,0 +1,5 @@ +package io.sphere.util + +trait Logging extends com.typesafe.scalalogging.StrictLogging { + protected val log = logger +} diff --git a/util-3/src/main/scala/Memoizer.scala b/util-3/src/main/scala/Memoizer.scala new file mode 100644 index 00000000..9c6ea674 --- /dev/null +++ b/util-3/src/main/scala/Memoizer.scala @@ -0,0 +1,28 @@ +package io.sphere.util + +import java.util.concurrent._ + +/** Straight port from the Java impl. of "Java Concurrency in Practice". */ +final class Memoizer[K, V](action: K => V) extends (K => V) { + private val cache = new ConcurrentHashMap[K, Future[V]] + def apply(k: K): V = { + while (true) { + var f = cache.get(k) + if (f == null) { + val eval = new Callable[V] { def call(): V = action(k) } + val ft = new FutureTask[V](eval) + f = cache.putIfAbsent(k, ft) + if (f == null) { + f = ft + ft.run() + } + } + try return f.get + catch { + case _: CancellationException => cache.remove(k, f) + case e: ExecutionException => throw e.getCause + } + } + sys.error("Failed to compute result.") + } +} diff --git a/util-3/src/main/scala/Money.scala b/util-3/src/main/scala/Money.scala new file mode 100644 index 00000000..b3ffdac7 --- /dev/null +++ b/util-3/src/main/scala/Money.scala @@ -0,0 +1,666 @@ +package io.sphere.util + +import language.implicitConversions +import java.math.MathContext +import java.text.NumberFormat +import java.util.{Currency, Locale} + +import cats.Monoid +import cats.data.ValidatedNel +import cats.syntax.validated._ + +import scala.math._ +import BigDecimal.RoundingMode._ +import scala.math.BigDecimal.RoundingMode +import ValidatedFlatMapFeature._ +import io.sphere.util.BaseMoney.bigDecimalToMoneyLong +import io.sphere.util.Money.ImplicitsDecimal.MoneyNotation + +class MoneyOverflowException extends RuntimeException("A Money operation resulted in an overflow.") + +sealed trait BaseMoney { + def `type`: String + + def currency: Currency + + // Use with CAUTION! will loose precision in case of a high precision money value + def centAmount: Long + + /** Normalized representation. + * + * for centPrecision: + * - centAmount: 1234 EUR + * - amount: 12.34 + * + * for highPrecision: preciseAmount: + * - 123456 EUR (with fractionDigits = 4) + * - amount: 12.3456 + */ + def amount: BigDecimal + + def fractionDigits: Int + + def toMoneyWithPrecisionLoss: Money + + def +(m: Money)(implicit mode: RoundingMode): BaseMoney + def +(m: HighPrecisionMoney)(implicit mode: RoundingMode): BaseMoney + def +(m: BaseMoney)(implicit mode: RoundingMode): BaseMoney + def +(m: BigDecimal)(implicit mode: RoundingMode): BaseMoney + + def -(m: Money)(implicit mode: RoundingMode): BaseMoney + def -(m: HighPrecisionMoney)(implicit mode: RoundingMode): BaseMoney + def -(m: BaseMoney)(implicit mode: RoundingMode): BaseMoney + def -(m: BigDecimal)(implicit mode: RoundingMode): BaseMoney + + def *(m: Money)(implicit mode: RoundingMode): BaseMoney + def *(m: HighPrecisionMoney)(implicit mode: RoundingMode): BaseMoney + def *(m: BaseMoney)(implicit mode: RoundingMode): BaseMoney + def *(m: BigDecimal)(implicit mode: RoundingMode): BaseMoney +} + +object BaseMoney { + val TypeField: String = "type" + + def requireSameCurrency(m1: BaseMoney, m2: BaseMoney): Unit = + require(m1.currency eq m2.currency, s"${m1.currency} != ${m2.currency}") + + def toScalaRoundingMode(mode: java.math.RoundingMode): RoundingMode.Value = + BigDecimal.RoundingMode(mode.ordinal) + + implicit def baseMoneyMonoid(implicit c: Currency, mode: RoundingMode): Monoid[BaseMoney] = + new Monoid[BaseMoney] { + def combine(x: BaseMoney, y: BaseMoney): BaseMoney = x + y + val empty: BaseMoney = Money.zero(c) + } + + private[util] def bigDecimalToMoneyLong(amount: BigDecimal): Long = + try amount.toLongExact + catch { case _: ArithmeticException => throw new MoneyOverflowException } +} + +/** Represents an amount of money in a certain currency. + * + * This implementation does not support fractional money units (eg a tenth cent). Amounts are + * always rounded to the nearest, smallest unit of the respective currency. The rounding mode can + * be specified using an implicit `BigDecimal.RoundingMode`. + * + * @param centAmount + * The amount in the smallest indivisible unit of the respective currency represented as a single + * Long value. + * @param currency + * The currency of the amount. + */ +case class Money private[util] (centAmount: Long, currency: Currency) + extends BaseMoney + with Ordered[Money] { + import Money._ + + private val centFactor: Double = 1 / pow(10, currency.getDefaultFractionDigits) + private val backwardsCompatibleRoundingModeForOperations = BigDecimal.RoundingMode.HALF_EVEN + + val `type`: String = TypeName + + override def fractionDigits: Int = currency.getDefaultFractionDigits + override lazy val amount: BigDecimal = BigDecimal(centAmount) * cachedCentFactor(fractionDigits) + + def withCentAmount(centAmount: Long): Money = + copy(centAmount = centAmount) + + def toHighPrecisionMoney(fractionDigits: Int): HighPrecisionMoney = + HighPrecisionMoney.fromMoney(this, fractionDigits) + + /** Creates a new Money instance with the same currency and the amount conforming to the given + * MathContext (scale and rounding mode). + */ + def apply(mc: MathContext): Money = + fromDecimalAmount(this.amount(mc), this.currency)(RoundingMode.HALF_EVEN) + + def +(m: Money)(implicit mode: RoundingMode): Money = { + BaseMoney.requireSameCurrency(this, m) + + fromDecimalAmount(this.amount + m.amount, this.currency)( + backwardsCompatibleRoundingModeForOperations) + } + + def +(m: HighPrecisionMoney)(implicit mode: RoundingMode): HighPrecisionMoney = + this.toHighPrecisionMoney(m.fractionDigits) + m + + def +(money: BaseMoney)(implicit mode: RoundingMode): BaseMoney = money match { + case m: Money => this + m + case m: HighPrecisionMoney => this + m + } + + def +(m: BigDecimal)(implicit mode: RoundingMode): Money = + this + fromDecimalAmount(m, this.currency) + + def -(m: Money)(implicit mode: RoundingMode): Money = { + BaseMoney.requireSameCurrency(this, m) + fromDecimalAmount(this.amount - m.amount, this.currency) + } + + def -(money: BaseMoney)(implicit mode: RoundingMode): BaseMoney = money match { + case m: Money => this - m + case m: HighPrecisionMoney => this - m + } + + def -(m: HighPrecisionMoney)(implicit mode: RoundingMode): HighPrecisionMoney = + this.toHighPrecisionMoney(m.fractionDigits) - m + + def -(m: BigDecimal)(implicit mode: RoundingMode): Money = + this - fromDecimalAmount(m, this.currency) + + def *(m: Money)(implicit mode: RoundingMode): Money = { + BaseMoney.requireSameCurrency(this, m) + this * m.amount + } + + def *(m: HighPrecisionMoney)(implicit mode: RoundingMode): HighPrecisionMoney = + this.toHighPrecisionMoney(m.fractionDigits) * m + + def *(money: BaseMoney)(implicit mode: RoundingMode): BaseMoney = money match { + case m: Money => this * m + case m: HighPrecisionMoney => this * m + } + + def *(m: BigDecimal)(implicit mode: RoundingMode): Money = + fromDecimalAmount((this.amount * m).setScale(this.amount.scale, mode), this.currency) + + /** Divide to integral value + remainder */ + def /%(m: BigDecimal)(implicit mode: RoundingMode): (Money, Money) = { + val (result, remainder) = this.amount /% m + + (fromDecimalAmount(result, this.currency), fromDecimalAmount(remainder, this.currency)) + } + + def %(m: Money)(implicit mode: RoundingMode): Money = this.remainder(m) + + def %(m: BigDecimal)(implicit mode: RoundingMode): Money = + this.remainder(fromDecimalAmount(m, this.currency)) + + def remainder(m: Money)(implicit mode: RoundingMode): Money = { + BaseMoney.requireSameCurrency(this, m) + + fromDecimalAmount(this.amount.remainder(m.amount), this.currency) + } + + def remainder(m: BigDecimal)(implicit mode: RoundingMode): Money = + this.remainder(fromDecimalAmount(m, this.currency)(RoundingMode.HALF_EVEN)) + + def unary_- : Money = + fromDecimalAmount(-this.amount, this.currency)(BigDecimal.RoundingMode.UNNECESSARY) + + /** Partitions this amount of money into several parts where the size of the individual parts are + * defined by the given ratios. The partitioning takes care of not losing or gaining any money by + * distributing any remaining "cents" evenly across the partitions. + * + *

Example: (0.05 EUR) partition (3,7) == Seq(0.02 EUR, 0.03 EUR)

+ */ + def partition(ratios: Int*): Seq[Money] = { + val total = ratios.sum + val amountInCents = BigInt(this.centAmount) + val amounts = ratios.map(amountInCents * _ / total) + var remainder = amounts.foldLeft(amountInCents)(_ - _) + amounts.map { amount => + remainder -= 1 + fromDecimalAmount( + BigDecimal(amount + (if (remainder >= 0) 1 else 0)) * centFactor, + this.currency)(backwardsCompatibleRoundingModeForOperations) + } + } + + def toMoneyWithPrecisionLoss: Money = this + + def compare(that: Money): Int = { + BaseMoney.requireSameCurrency(this, that) + this.centAmount.compare(that.centAmount) + } + + override def toString: String = Money.toString(centAmount, fractionDigits, currency) + + def toString(nf: NumberFormat, locale: Locale): String = { + require(nf.getCurrency eq this.currency) + nf.format(this.amount.doubleValue) + " " + this.currency.getSymbol(locale) + } +} + +object Money { + object ImplicitsDecimal { + final implicit class MoneyNotation(val amount: BigDecimal) extends AnyVal { + def EUR: Money = Money.EUR(amount) + def USD: Money = Money.USD(amount) + def GBP: Money = Money.GBP(amount) + def JPY: Money = Money.JPY(amount) + } + + implicit def doubleMoneyNotation(amount: Double): MoneyNotation = + new ImplicitsDecimal.MoneyNotation(BigDecimal(amount)) + } + + object ImplicitsString { + implicit def stringMoneyNotation(amount: String): MoneyNotation = + new ImplicitsDecimal.MoneyNotation(BigDecimal(amount)) + } + + private def decimalAmountWithCurrencyAndHalfEvenRounding(amount: BigDecimal, currency: String) = + fromDecimalAmount(amount, Currency.getInstance(currency))(BigDecimal.RoundingMode.HALF_EVEN) + + def EUR(amount: BigDecimal): Money = decimalAmountWithCurrencyAndHalfEvenRounding(amount, "EUR") + def USD(amount: BigDecimal): Money = decimalAmountWithCurrencyAndHalfEvenRounding(amount, "USD") + def GBP(amount: BigDecimal): Money = decimalAmountWithCurrencyAndHalfEvenRounding(amount, "GBP") + def JPY(amount: BigDecimal): Money = decimalAmountWithCurrencyAndHalfEvenRounding(amount, "JPY") + + final val CurrencyCodeField: String = "currencyCode" + final val CentAmountField: String = "centAmount" + final val FractionDigitsField: String = "fractionDigits" + final val TypeName: String = "centPrecision" + + def fromDecimalAmount(amount: BigDecimal, currency: Currency)(implicit + mode: RoundingMode): Money = { + val fractionDigits = currency.getDefaultFractionDigits + val centAmountBigDecimal = amount * cachedCentPower(fractionDigits) + val centAmountBigDecimalZeroScale = centAmountBigDecimal.setScale(0, mode) + Money(bigDecimalToMoneyLong(centAmountBigDecimalZeroScale), currency) + } + + def apply(amount: BigDecimal, currency: Currency): Money = { + println("this is called") + require( + amount.scale == currency.getDefaultFractionDigits, + "The scale of the given amount does not match the scale of the provided currency." + + " - " + amount.scale + " <-> " + currency.getDefaultFractionDigits + ) + fromDecimalAmount(amount, currency)(BigDecimal.RoundingMode.UNNECESSARY) + } + + private final val bdOne: BigDecimal = BigDecimal(1) + final val bdTen: BigDecimal = BigDecimal(10) + + private final val centPowerZeroFractionDigit = bdOne + private final val centPowerOneFractionDigit = bdTen + private final val centPowerTwoFractionDigit = bdTen.pow(2) + private final val centPowerThreeFractionDigit = bdTen.pow(3) + private final val centPowerFourFractionDigit = bdTen.pow(4) + + private[util] def cachedCentPower(currencyFractionDigits: Int): BigDecimal = + currencyFractionDigits match { + case 0 => centPowerZeroFractionDigit + case 1 => centPowerOneFractionDigit + case 2 => centPowerTwoFractionDigit + case 3 => centPowerThreeFractionDigit + case 4 => centPowerFourFractionDigit + case other => bdTen.pow(other) + } + + private val centFactorZeroFractionDigit = bdOne / bdTen.pow(0) + private val centFactorOneFractionDigit = bdOne / bdTen.pow(1) + private val centFactorTwoFractionDigit = bdOne / bdTen.pow(2) + private val centFactorThreeFractionDigit = bdOne / bdTen.pow(3) + private val centFactorFourFractionDigit = bdOne / bdTen.pow(4) + + private[util] def cachedCentFactor(currencyFractionDigits: Int): BigDecimal = + currencyFractionDigits match { + case 0 => centFactorZeroFractionDigit + case 1 => centFactorOneFractionDigit + case 2 => centFactorTwoFractionDigit + case 3 => centFactorThreeFractionDigit + case 4 => centFactorFourFractionDigit + case other => bdOne / bdTen.pow(other) + } + + def fromCentAmount(centAmount: Long, currency: Currency): Money = + new Money(centAmount, currency) + + private val cachedZeroEUR = fromCentAmount(0L, Currency.getInstance("EUR")) + private val cachedZeroUSD = fromCentAmount(0L, Currency.getInstance("USD")) + private val cachedZeroGBP = fromCentAmount(0L, Currency.getInstance("GBP")) + private val cachedZeroJPY = fromCentAmount(0L, Currency.getInstance("JPY")) + + def zero(currency: Currency): Money = + currency.getCurrencyCode match { + case "EUR" => cachedZeroEUR + case "USD" => cachedZeroUSD + case "GBP" => cachedZeroGBP + case "JPY" => cachedZeroJPY + case _ => fromCentAmount(0L, currency) + } + + implicit def moneyMonoid(implicit c: Currency, mode: RoundingMode): Monoid[Money] = + new Monoid[Money] { + def combine(x: Money, y: Money): Money = x + y + val empty: Money = Money.zero(c) + } + + def toString(amount: Long, fractionDigits: Int, currency: Currency): String = { + val amountDigits = amount.toString.toList + val leadingZerosLength = fractionDigits - amountDigits.length + 1 + val leadingZeros = List.fill(leadingZerosLength)('0') + val allDigits = leadingZeros ::: amountDigits + val radixPosition = allDigits.length - fractionDigits + val (integer, fractional) = allDigits.splitAt(radixPosition) + if (fractional.nonEmpty) + s"${integer.mkString}.${fractional.mkString} ${currency.getCurrencyCode}" + else + s"${integer.mkString} ${currency.getCurrencyCode}" + } +} + +case class HighPrecisionMoney private ( + preciseAmount: Long, + fractionDigits: Int, + centAmount: Long, + currency: Currency) + extends BaseMoney + with Ordered[Money] { + import HighPrecisionMoney._ + + require( + fractionDigits >= currency.getDefaultFractionDigits, + "`fractionDigits` should be >= than the default fraction digits of the currency.") + + val `type`: String = TypeName + + lazy val amount: BigDecimal = + (BigDecimal(preciseAmount) * factor(fractionDigits)).setScale(fractionDigits) + + def withFractionDigits(fd: Int)(implicit mode: RoundingMode): HighPrecisionMoney = { + val scaledAmount = amount.setScale(fd, mode) + val newCentAmount = roundToCents(scaledAmount, currency) + HighPrecisionMoney(amountToPreciseAmount(scaledAmount, fd), fd, newCentAmount, currency) + } + + def updateCentAmountWithRoundingMode(implicit mode: RoundingMode): HighPrecisionMoney = + copy(centAmount = roundToCents(amount, currency)) + + def +(other: HighPrecisionMoney)(implicit mode: RoundingMode): HighPrecisionMoney = + calc(this, other, _ + _) + + def +(m: Money)(implicit mode: RoundingMode): HighPrecisionMoney = + this + m.toHighPrecisionMoney(fractionDigits) + + def +(money: BaseMoney)(implicit mode: RoundingMode): HighPrecisionMoney = money match { + case m: Money => this + m + case m: HighPrecisionMoney => this + m + } + + def +(other: BigDecimal)(implicit mode: RoundingMode): HighPrecisionMoney = + this + fromDecimalAmount(other, this.fractionDigits, this.currency) + + def -(other: HighPrecisionMoney)(implicit mode: RoundingMode): HighPrecisionMoney = + calc(this, other, _ - _) + + def -(m: Money)(implicit mode: RoundingMode): HighPrecisionMoney = + this - m.toHighPrecisionMoney(fractionDigits) + + def -(money: BaseMoney)(implicit mode: RoundingMode): HighPrecisionMoney = money match { + case m: Money => this - m + case m: HighPrecisionMoney => this - m + } + + def -(other: BigDecimal)(implicit mode: RoundingMode): HighPrecisionMoney = + this - fromDecimalAmount(other, this.fractionDigits, this.currency) + + def *(other: HighPrecisionMoney)(implicit mode: RoundingMode): HighPrecisionMoney = + calc(this, other, _ * _) + + def *(m: Money)(implicit mode: RoundingMode): HighPrecisionMoney = + this * m.toHighPrecisionMoney(fractionDigits) + + def *(money: BaseMoney)(implicit mode: RoundingMode): HighPrecisionMoney = money match { + case m: Money => this * m + case m: HighPrecisionMoney => this * m + } + + def *(other: BigDecimal)(implicit mode: RoundingMode): HighPrecisionMoney = + this * fromDecimalAmount(other, this.fractionDigits, this.currency) + + /** Divide to integral value + remainder */ + def /%(m: BigDecimal)(implicit mode: RoundingMode): (HighPrecisionMoney, HighPrecisionMoney) = { + val (result, remainder) = this.amount /% m + + fromDecimalAmount(result, fractionDigits, this.currency) -> + fromDecimalAmount(remainder, fractionDigits, this.currency) + } + + def %(other: HighPrecisionMoney)(implicit mode: RoundingMode): HighPrecisionMoney = + this.remainder(other) + + def %(m: Money)(implicit mode: RoundingMode): HighPrecisionMoney = + this.remainder(m.toHighPrecisionMoney(fractionDigits)) + + def %(other: BigDecimal)(implicit mode: RoundingMode): HighPrecisionMoney = + this.remainder(fromDecimalAmount(other, this.fractionDigits, this.currency)) + + def remainder(other: HighPrecisionMoney)(implicit mode: RoundingMode): HighPrecisionMoney = + calc(this, other, _ remainder _) + + def remainder(other: BigDecimal)(implicit mode: RoundingMode): HighPrecisionMoney = + this.remainder(fromDecimalAmount(other, this.fractionDigits, this.currency)) + + def unary_- : HighPrecisionMoney = + fromDecimalAmount(-this.amount, this.fractionDigits, this.currency)( + BigDecimal.RoundingMode.UNNECESSARY) + + /** Partitions this amount of money into several parts where the size of the individual parts are + * defined by the given ratios. The partitioning takes care of not losing or gaining any money by + * distributing any remaining "cents" evenly across the partitions. + * + *

Example: (0.05 EUR) partition (3,7) == Seq(0.02 EUR, 0.03 EUR)

+ */ + def partition(ratios: Int*)(implicit mode: RoundingMode): Seq[HighPrecisionMoney] = { + val total = ratios.sum + val factor = Money.cachedCentFactor(fractionDigits) + val amountAsInt = BigInt(this.preciseAmount) + val portionAmounts = ratios.map(amountAsInt * _ / total) + var remainder = portionAmounts.foldLeft(amountAsInt)(_ - _) + + portionAmounts.map { portionAmount => + remainder -= 1 + + fromDecimalAmount( + BigDecimal(portionAmount + (if (remainder >= 0) 1 else 0)) * factor, + this.fractionDigits, + this.currency) + } + } + + def toMoneyWithPrecisionLoss: Money = + Money.fromCentAmount(this.centAmount, currency) + + def compare(other: Money): Int = { + BaseMoney.requireSameCurrency(this, other) + + this.amount.compare(other.amount) + } + + override def toString: String = Money.toString(preciseAmount, fractionDigits, currency) + + def toString(nf: NumberFormat, locale: Locale): String = { + require(nf.getCurrency eq this.currency) + + nf.format(this.amount.doubleValue) + " " + this.currency.getSymbol(locale) + } +} + +object HighPrecisionMoney { + object ImplicitsDecimal { + final implicit class HighPrecisionMoneyNotation(val amount: BigDecimal) extends AnyVal { + def EUR: HighPrecisionMoney = HighPrecisionMoney.EUR(amount) + def USD: HighPrecisionMoney = HighPrecisionMoney.USD(amount) + def GBP: HighPrecisionMoney = HighPrecisionMoney.GBP(amount) + def JPY: HighPrecisionMoney = HighPrecisionMoney.JPY(amount) + } + } + + object ImplicitsDecimalPrecise { + final implicit class HighPrecisionPreciseMoneyNotation(val amount: BigDecimal) extends AnyVal { + def EUR_PRECISE(precision: Int): HighPrecisionMoney = + HighPrecisionMoney.EUR(amount, Some(precision)) + def USD_PRECISE(precision: Int): HighPrecisionMoney = + HighPrecisionMoney.USD(amount, Some(precision)) + def GBP_PRECISE(precision: Int): HighPrecisionMoney = + HighPrecisionMoney.GBP(amount, Some(precision)) + def JPY_PRECISE(precision: Int): HighPrecisionMoney = + HighPrecisionMoney.JPY(amount, Some(precision)) + } + } + + object ImplicitsString { + implicit def stringMoneyNotation(amount: String): ImplicitsDecimal.HighPrecisionMoneyNotation = + new ImplicitsDecimal.HighPrecisionMoneyNotation(BigDecimal(amount)) + } + + object ImplicitsStringPrecise { + implicit def stringPreciseMoneyNotation( + amount: String): ImplicitsDecimalPrecise.HighPrecisionPreciseMoneyNotation = + new ImplicitsDecimalPrecise.HighPrecisionPreciseMoneyNotation(BigDecimal(amount)) + } + + def EUR(amount: BigDecimal, fractionDigits: Option[Int] = None): HighPrecisionMoney = + simpleValueMeantToBeUsedOnlyInTests(amount, "EUR", fractionDigits) + def USD(amount: BigDecimal, fractionDigits: Option[Int] = None): HighPrecisionMoney = + simpleValueMeantToBeUsedOnlyInTests(amount, "USD", fractionDigits) + def GBP(amount: BigDecimal, fractionDigits: Option[Int] = None): HighPrecisionMoney = + simpleValueMeantToBeUsedOnlyInTests(amount, "GBP", fractionDigits) + def JPY(amount: BigDecimal, fractionDigits: Option[Int] = None): HighPrecisionMoney = + simpleValueMeantToBeUsedOnlyInTests(amount, "JPY", fractionDigits) + + val CurrencyCodeField: String = "currencyCode" + val CentAmountField: String = "centAmount" + val PreciseAmountField: String = "preciseAmount" + val FractionDigitsField: String = "fractionDigits" + + val TypeName: String = "highPrecision" + val MaxFractionDigits = 20 + + private def simpleValueMeantToBeUsedOnlyInTests( + amount: BigDecimal, + currencyCode: String, + fractionDigits: Option[Int]): HighPrecisionMoney = { + val currency = Currency.getInstance(currencyCode) + val fd = fractionDigits.getOrElse(currency.getDefaultFractionDigits) + + fromDecimalAmount(amount, fd, currency)(BigDecimal.RoundingMode.HALF_EVEN) + } + + def roundToCents(amount: BigDecimal, currency: Currency)(implicit mode: RoundingMode): Long = + bigDecimalToMoneyLong( + amount.setScale(currency.getDefaultFractionDigits, mode) / centFactor(currency)) + + def sameScale(m1: HighPrecisionMoney, m2: HighPrecisionMoney): (BigDecimal, BigDecimal, Int) = { + val newFractionDigits = math.max(m1.fractionDigits, m2.fractionDigits) + + def scale(m: HighPrecisionMoney, s: Int) = + if (m.fractionDigits < s) m.amount.setScale(s) + else if (m.fractionDigits == s) m.amount + else throw new IllegalStateException("Downscale is not allowed/expected at this point!") + + (scale(m1, newFractionDigits), scale(m2, newFractionDigits), newFractionDigits) + } + + def calc( + m1: HighPrecisionMoney, + m2: HighPrecisionMoney, + fn: (BigDecimal, BigDecimal) => BigDecimal)(implicit + mode: RoundingMode): HighPrecisionMoney = { + BaseMoney.requireSameCurrency(m1, m2) + + val (a1, a2, fd) = sameScale(m1, m2) + + fromDecimalAmount(fn(a1, a2), fd, m1.currency) + } + + def factor(fractionDigits: Int): BigDecimal = Money.cachedCentFactor(fractionDigits) + def centFactor(currency: Currency): BigDecimal = factor(currency.getDefaultFractionDigits) + + private def amountToPreciseAmount(amount: BigDecimal, fractionDigits: Int): Long = + bigDecimalToMoneyLong(amount * Money.cachedCentPower(fractionDigits)) + + def fromDecimalAmount(amount: BigDecimal, fractionDigits: Int, currency: Currency)(implicit + mode: RoundingMode): HighPrecisionMoney = { + val scaledAmount = amount.setScale(fractionDigits, mode) + val preciseAmount = amountToPreciseAmount(scaledAmount, fractionDigits) + val newCentAmount = roundToCents(scaledAmount, currency) + HighPrecisionMoney(preciseAmount, fractionDigits, newCentAmount, currency) + } + + private def centToPreciseAmount( + centAmount: Long, + fractionDigits: Int, + currency: Currency): Long = { + val centDigits = fractionDigits - currency.getDefaultFractionDigits + if (centDigits >= 19) + throw new IllegalArgumentException("Cannot represent number bigger than 10^19 with a Long") + else + Math.pow(10, centDigits).toLong * centAmount + } + + def fromCentAmount( + centAmount: Long, + fractionDigits: Int, + currency: Currency): HighPrecisionMoney = + HighPrecisionMoney( + centToPreciseAmount(centAmount, fractionDigits, currency), + fractionDigits, + centAmount, + currency) + + def zero(fractionDigits: Int, currency: Currency): HighPrecisionMoney = + fromCentAmount(0L, fractionDigits, currency) + + /* centAmount provides an escape hatch in cases where the default rounding mode is not applicable */ + def fromPreciseAmount( + preciseAmount: Long, + fractionDigits: Int, + currency: Currency, + centAmount: Option[Long]): ValidatedNel[String, HighPrecisionMoney] = + for { + fd <- validateFractionDigits(fractionDigits, currency) + amount = BigDecimal(preciseAmount) * factor(fd) + scaledAmount = amount.setScale(fd, BigDecimal.RoundingMode.UNNECESSARY) + ca <- validateCentAmount(scaledAmount, centAmount, currency) + // TODO: revisit this part! the rounding mode might be dynamic and configured elsewhere + actualCentAmount = ca.getOrElse( + roundToCents(scaledAmount, currency)(BigDecimal.RoundingMode.HALF_EVEN)) + } yield HighPrecisionMoney(preciseAmount, fd, actualCentAmount, currency) + + private def validateFractionDigits( + fractionDigits: Int, + currency: Currency): ValidatedNel[String, Int] = + if (fractionDigits <= currency.getDefaultFractionDigits) + s"fractionDigits must be > ${currency.getDefaultFractionDigits} (default fraction digits defined by currency ${currency.getCurrencyCode}).".invalidNel + else if (fractionDigits > MaxFractionDigits) + s"fractionDigits must be <= $MaxFractionDigits.".invalidNel + else + fractionDigits.validNel + + private def validateCentAmount( + amount: BigDecimal, + centAmount: Option[Long], + currency: Currency): ValidatedNel[String, Option[Long]] = + centAmount match { + case Some(actual) => + val min = roundToCents(amount, currency)(RoundingMode.FLOOR) + val max = roundToCents(amount, currency)(RoundingMode.CEILING) + + if (actual < min || actual > max) + s"centAmount must be correctly rounded preciseAmount (a number between $min and $max).".invalidNel + else + centAmount.validNel + + case _ => + centAmount.validNel + } + + def fromMoney(money: Money, fractionDigits: Int): HighPrecisionMoney = + HighPrecisionMoney( + centToPreciseAmount(money.centAmount, fractionDigits, money.currency), + fractionDigits, + money.centAmount, + money.currency) + + def monoid(fractionDigits: Int, c: Currency)(implicit + mode: RoundingMode): Monoid[HighPrecisionMoney] = new Monoid[HighPrecisionMoney] { + def combine(x: HighPrecisionMoney, y: HighPrecisionMoney): HighPrecisionMoney = x + y + val empty: HighPrecisionMoney = HighPrecisionMoney.zero(fractionDigits, c) + } +} diff --git a/util-3/src/main/scala/Reflect.scala b/util-3/src/main/scala/Reflect.scala new file mode 100644 index 00000000..bb299b67 --- /dev/null +++ b/util-3/src/main/scala/Reflect.scala @@ -0,0 +1,61 @@ +package io.sphere.util + +import org.json4s.scalap.scalasig._ + +object Reflect extends Logging { + case class CaseClassMeta(fields: IndexedSeq[CaseClassFieldMeta]) + case class CaseClassFieldMeta(name: String, default: Option[Any] = None) + + /** Obtains minimal meta information about a case class or object via scalap. The meta information + * contains a list of names and default values which represent the arguments of the case class + * constructor and their default values, in the order they are defined. + * + * Note: Does not work for case classes or objects nested in other classes or traits (nesting + * inside other objects is fine). Note: Only a single default value is obtained for each field. + * Thus avoid default values that are different on each invocation (e.g. new DateTime()). In + * other words, the case class constructors should be pure functions. + */ + val getCaseClassMeta = new Memoizer[Class[?], CaseClassMeta](clazz => { + logger.trace( + "Initializing reflection metadata for case class or object %s".format(clazz.getName)) + CaseClassMeta(getCaseClassFieldMeta(clazz)) + }) + + private def getCompanionClass(clazz: Class[?]): Class[?] = + Class.forName(clazz.getName + "$", true, clazz.getClassLoader) + private def getCompanionObject(companionClass: Class[?]): Object = + companionClass.getField("MODULE$").get(null) + private def getCaseClassFieldMeta(clazz: Class[?]): IndexedSeq[CaseClassFieldMeta] = + if (clazz.getName.endsWith("$")) IndexedSeq.empty[CaseClassFieldMeta] + else { + val companionClass = getCompanionClass(clazz) + val companionObject = getCompanionObject(companionClass) + + val maybeSym = clazz.getName.split("\\$") match { + case Array(_) => ScalaSigParser.parse(clazz).flatMap(_.topLevelClasses.headOption) + case Array(h, t @ _*) => + val name = t.last + val topSymbol = ScalaSigParser.parse(Class.forName(h, true, clazz.getClassLoader)) + topSymbol.flatMap(_.symbols.collectFirst { case s: ClassSymbol if s.name == name => s }) + } + + val sym = maybeSym.getOrElse { + throw new IllegalArgumentException( + "Unable to find class symbol through ScalaSigParser for class %s." + .format(clazz.getName)) + } + + sym.children.iterator + .collect { case m: MethodSymbol if m.isCaseAccessor && !m.isPrivate => m } + .zipWithIndex + .map { case (ms, idx) => + val defaultValue = + try Some(companionClass.getMethod("apply$default$" + (idx + 1)).invoke(companionObject)) + catch { + case _: NoSuchMethodException => None + } + CaseClassFieldMeta(ms.name, defaultValue) + } + .toIndexedSeq + } +} diff --git a/util-3/src/main/scala/ValidatedFlatMap.scala b/util-3/src/main/scala/ValidatedFlatMap.scala new file mode 100644 index 00000000..9c9f1991 --- /dev/null +++ b/util-3/src/main/scala/ValidatedFlatMap.scala @@ -0,0 +1,23 @@ +package io.sphere.util + +import cats.data.Validated + +class ValidatedFlatMap[E, A](val v: Validated[E, A]) extends AnyVal { + def flatMap[EE >: E, B](f: A => Validated[EE, B]): Validated[EE, B] = + v.andThen(f) +} + +/** Cats [[Validated]] does not provide `flatMap` because its purpose is to accumulate errors. + * + * To combine [[Validated]] in for-comprehension, it is possible to import this implicit conversion + * - with the knowledge that the `flatMap` short-circuits errors. + * http://typelevel.org/cats/datatypes/validated.html + */ +object ValidatedFlatMapFeature { + import scala.language.implicitConversions + + @inline implicit def ValidationFlatMapRequested[E, A]( + d: Validated[E, A]): ValidatedFlatMap[E, A] = + new ValidatedFlatMap(d) + +} diff --git a/util-3/src/test/scala/DomainObjectsGen.scala b/util-3/src/test/scala/DomainObjectsGen.scala new file mode 100644 index 00000000..b654f020 --- /dev/null +++ b/util-3/src/test/scala/DomainObjectsGen.scala @@ -0,0 +1,25 @@ +package io.sphere.util + +import java.util.Currency + +import org.scalacheck.Gen + +import scala.jdk.CollectionConverters._ + +object DomainObjectsGen { + + private val currency: Gen[Currency] = + Gen.oneOf(Currency.getAvailableCurrencies.asScala.toSeq) + + val money: Gen[Money] = for { + currency <- currency + amount <- Gen.chooseNum[Long](Long.MinValue, Long.MaxValue) + } yield Money(amount, currency) + + val highPrecisionMoney: Gen[HighPrecisionMoney] = for { + money <- money + } yield HighPrecisionMoney.fromMoney(money, money.currency.getDefaultFractionDigits) + + val baseMoney: Gen[BaseMoney] = Gen.oneOf(money, highPrecisionMoney) + +} diff --git a/util-3/src/test/scala/HighPrecisionMoneySpec.scala b/util-3/src/test/scala/HighPrecisionMoneySpec.scala new file mode 100644 index 00000000..f73d181c --- /dev/null +++ b/util-3/src/test/scala/HighPrecisionMoneySpec.scala @@ -0,0 +1,218 @@ +package io.sphere.util + +import java.util.Currency +import cats.data.Validated.Invalid +import io.sphere.util.HighPrecisionMoney.ImplicitsDecimalPrecise.HighPrecisionPreciseMoneyNotation +import org.scalatest.funspec.AnyFunSpec +import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks +import org.scalatest.matchers.must.Matchers + +import scala.collection.mutable.ArrayBuffer +import scala.language.postfixOps +import scala.math.BigDecimal + +class HighPrecisionMoneySpec extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks { + import HighPrecisionMoney.ImplicitsString._ + import HighPrecisionMoney.ImplicitsStringPrecise._ + + implicit val defaultRoundingMode: BigDecimal.RoundingMode.Value = + BigDecimal.RoundingMode.HALF_EVEN + + val Euro: Currency = Currency.getInstance("EUR") + + describe("High Precision Money") { + + it("should allow creation of high precision money") { + ("0.01".EUR) must equal("0.01".EUR) + } + + it( + "should not allow creation of high precision money with less fraction digits than the currency has") { + val thrown = intercept[IllegalArgumentException] { + "0.01".EUR_PRECISE(1) + } + + assert( + thrown.getMessage == "requirement failed: `fractionDigits` should be >= than the default fraction digits of the currency.") + } + + it("should convert precise amount to long value correctly") { + "0.0001".EUR_PRECISE(4).preciseAmount must equal(1) + } + + it("should reduce fraction digits as expected") { + "0.0001".EUR_PRECISE(4).withFractionDigits(2).preciseAmount must equal(0) + } + + it("should support the unary '-' operator.") { + -"0.01".EUR_PRECISE(2) must equal("-0.01".EUR_PRECISE(2)) + } + + it("should throw error on overflow in the unary '-' operator.") { + a[MoneyOverflowException] must be thrownBy { + -(BigDecimal(Long.MinValue) / 1000).EUR_PRECISE(3) + } + } + + it("should support the binary '+' operator.") { + ("0.001".EUR_PRECISE(3)) + ("0.002".EUR_PRECISE(3)) must equal( + "0.003".EUR_PRECISE(3) + ) + + ("0.005".EUR_PRECISE(3)) + Money.fromDecimalAmount(BigDecimal("0.01"), Euro) must equal( + "0.015".EUR_PRECISE(3) + ) + + ("0.005".EUR_PRECISE(3)) + BigDecimal("0.005") must equal( + "0.010".EUR_PRECISE(3) + ) + } + + it("should throw error on overflow in the binary '+' operator.") { + a[MoneyOverflowException] must be thrownBy { + (BigDecimal(Long.MaxValue) / 1000).EUR_PRECISE(3) + 1 + } + } + + it("should support the binary '-' operator.") { + ("0.002".EUR_PRECISE(3)) - ("0.001".EUR_PRECISE(3)) must equal( + "0.001".EUR_PRECISE(3) + ) + + ("0.015".EUR_PRECISE(3)) - Money.fromDecimalAmount(BigDecimal("0.01"), Euro) must equal( + "0.005".EUR_PRECISE(3) + ) + + ("0.005".EUR_PRECISE(3)) - BigDecimal("0.005") must equal( + "0.000".EUR_PRECISE(3) + ) + } + + it("should throw error on overflow in the binary '-' operator.") { + a[MoneyOverflowException] must be thrownBy { + (BigDecimal(Long.MinValue) / 1000).EUR_PRECISE(3) - 1 + } + } + + it("should support the binary '*' operator.") { + ("0.002".EUR_PRECISE(3)) * ("5.00".EUR_PRECISE(2)) must equal( + "0.010".EUR_PRECISE(3) + ) + + ("0.015".EUR_PRECISE(3)) * Money.fromDecimalAmount(BigDecimal("100.00"), Euro) must equal( + "1.500".EUR_PRECISE(3) + ) + + ("0.005".EUR_PRECISE(3)) * BigDecimal("0.005") must equal( + "0.000".EUR_PRECISE(3) + ) + } + + it("should throw error on overflow in the binary '*' operator.") { + a[MoneyOverflowException] must be thrownBy { + (BigDecimal(Long.MaxValue / 1000) / 2 + 1).EUR_PRECISE(3) * 2 + } + } + + it("should support the binary '%' operator.") { + ("0.010".EUR_PRECISE(3)) % ("5.00".EUR_PRECISE(2)) must equal( + "0.010".EUR_PRECISE(3) + ) + + ("100.000".EUR_PRECISE(3)) % Money.fromDecimalAmount(BigDecimal("100.00"), Euro) must equal( + "0.000".EUR_PRECISE(3) + ) + + ("0.015".EUR_PRECISE(3)) % BigDecimal("0.002") must equal( + "0.001".EUR_PRECISE(3) + ) + } + + it("should throw error on overflow in the binary '%' operator.") { + noException must be thrownBy { + BigDecimal(Long.MaxValue / 1000).EUR_PRECISE(3) % 0.5 + } + } + + it("should support the binary '/%' operator.") { + "10.000".EUR_PRECISE(3)./%(3.00) must equal( + ("3.000".EUR_PRECISE(3), "1.000".EUR_PRECISE(3)) + ) + } + + it("should throw error on overflow in the binary '/%' operator.") { + a[MoneyOverflowException] must be thrownBy { + BigDecimal(Long.MaxValue / 1000).EUR_PRECISE(3) /% 0.5 + } + } + + it("should support the remainder operator.") { + "10.000".EUR_PRECISE(3).remainder(3.00) must equal("1.000".EUR_PRECISE(3)) + + "10.000".EUR_PRECISE(3).remainder("3.000".EUR_PRECISE(3)) must equal("1.000".EUR_PRECISE(3)) + } + + it("should not overflow when getting the remainder of a division ('%').") { + noException must be thrownBy { + BigDecimal(Long.MaxValue / 1000).EUR_PRECISE(3).remainder(0.5) + } + } + + it("should partition the value properly.") { + "10.000".EUR_PRECISE(3).partition(1, 2, 3) must equal( + ArrayBuffer( + "1.667".EUR_PRECISE(3), + "3.333".EUR_PRECISE(3), + "5.000".EUR_PRECISE(3) + ) + ) + } + + it("should validate fractionDigits (min)") { + val Invalid(errors) = HighPrecisionMoney.fromPreciseAmount(123456L, 1, Euro, None): @unchecked + + errors.toList must be( + List("fractionDigits must be > 2 (default fraction digits defined by currency EUR).")) + } + + it("should validate fractionDigits (max)") { + val Invalid(errors) = + HighPrecisionMoney.fromPreciseAmount(123456L, 100, Euro, None): @unchecked + + errors.toList must be(List("fractionDigits must be <= 20.")) + } + + it("should validate centAmount") { + val Invalid(errors) = + HighPrecisionMoney.fromPreciseAmount(123456L, 4, Euro, Some(1)): @unchecked + + errors.toList must be( + List( + "centAmount must be correctly rounded preciseAmount (a number between 1234 and 1235).")) + } + + it("should provide convenient toString") { + "10.000".EUR_PRECISE(3).toString must be("10.000 EUR") + "0.100".EUR_PRECISE(3).toString must be("0.100 EUR") + "0.010".EUR_PRECISE(3).toString must be("0.010 EUR") + "0.000".EUR_PRECISE(3).toString must be("0.000 EUR") + "94.500".EUR_PRECISE(3).toString must be("94.500 EUR") + "94".JPY_PRECISE(0).toString must be("94 JPY") + } + + it("should not fail on toString") { + forAll(DomainObjectsGen.highPrecisionMoney) { m => + m.toString + } + } + + it("should fail on too big fraction decimal") { + val thrown = intercept[IllegalArgumentException] { + val tooManyDigits = Euro.getDefaultFractionDigits + 19 + HighPrecisionMoney.fromCentAmount(100003, tooManyDigits, Euro) + } + + assert(thrown.getMessage == "Cannot represent number bigger than 10^19 with a Long") + } + } +} diff --git a/util-3/src/test/scala/LangTagSpec.scala b/util-3/src/test/scala/LangTagSpec.scala new file mode 100644 index 00000000..091fd6d9 --- /dev/null +++ b/util-3/src/test/scala/LangTagSpec.scala @@ -0,0 +1,27 @@ +package io.sphere.util + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.must.Matchers + +import scala.language.postfixOps + +class LangTagSpec extends AnyFunSpec with Matchers { + describe("LangTag") { + it("should accept valid language tags") { + LangTag.unapply("de").isEmpty must be(false) + LangTag.unapply("fr").isEmpty must be(false) + LangTag.unapply("de-DE").isEmpty must be(false) + LangTag.unapply("de-AT").isEmpty must be(false) + LangTag.unapply("de-CH").isEmpty must be(false) + LangTag.unapply("fr-FR").isEmpty must be(false) + LangTag.unapply("fr-CA").isEmpty must be(false) + LangTag.unapply("he-IL-u-ca-hebrew-tz-jeruslm").isEmpty must be(false) + } + + it("should not accept invalid language tags") { + LangTag.unapply(" de").isEmpty must be(true) + LangTag.unapply("de_DE").isEmpty must be(true) + LangTag.unapply("e-DE").isEmpty must be(true) + } + } +} diff --git a/util-3/src/test/scala/MoneySpec.scala b/util-3/src/test/scala/MoneySpec.scala new file mode 100644 index 00000000..7d4c1a4a --- /dev/null +++ b/util-3/src/test/scala/MoneySpec.scala @@ -0,0 +1,162 @@ +package io.sphere.util + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.must.Matchers +import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks + +import scala.language.postfixOps +import scala.math.BigDecimal + +class MoneySpec extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks { + import Money.ImplicitsDecimal._ + import Money._ + + implicit val mode: BigDecimal.RoundingMode.Value = BigDecimal.RoundingMode.UNNECESSARY + + def euroCents(cents: Long): Money = EUR(0).withCentAmount(cents) + + describe("Money") { + it("should have value semantics.") { + (1.23 EUR) must equal(1.23 EUR) + } + + it( + "should default to HALF_EVEN rounding mode when using monetary notation and use provided rounding mode when performing operations.") { + implicit val mode = BigDecimal.RoundingMode.HALF_EVEN + + (1.001 EUR) must equal(1.00 EUR) + (1.005 EUR) must equal(1.00 EUR) + (1.015 EUR) must equal(1.02 EUR) + ((1.00 EUR) + 0.001) must equal(1.00 EUR) + ((1.00 EUR) + 0.005) must equal(1.00 EUR) + ((1.00 EUR) + 0.015) must equal(1.02 EUR) + ((1.00 EUR) - 0.005) must equal(1.00 EUR) + ((1.00 EUR) - 0.015) must equal(0.98 EUR) + ((1.00 EUR) + 0.0115) must equal(1.01 EUR) + } + + it( + "should not accept an amount with an invalid scale for the used currency when using the constructor directly.") { + an[IllegalArgumentException] must be thrownBy { + Money(1.0001, java.util.Currency.getInstance("EUR")) + } + } + + it("should not be prone to common rounding errors known from floating point numbers.") { + var m = 0.00 EUR + + for (i <- 1 to 10) m = m + 0.10 + + m must equal(1.00 EUR) + } + + it("should support the unary '-' operator.") { + -EUR(1.00) must equal(-1.00 EUR) + } + + it("should throw error on overflow in the unary '-' operator.") { + a[MoneyOverflowException] must be thrownBy { + -euroCents(Long.MinValue) + } + } + + it("should support the binary '+' operator.") { + (1.42 EUR) + (1.58 EUR) must equal(3.00 EUR) + } + + it("should support the binary '+' operator on different currencies.") { + an[IllegalArgumentException] must be thrownBy { + (1.42 EUR) + (1.58 USD) + } + } + + it("should throw error on overflow in the binary '+' operator.") { + a[MoneyOverflowException] must be thrownBy { + euroCents(Long.MaxValue) + 1 + } + } + + it("should support the binary '-' operator.") { + (1.33 EUR) - (0.33 EUR) must equal(1.00 EUR) + } + + it("should throw error on overflow in the binary '-' operator.") { + a[MoneyOverflowException] must be thrownBy { + euroCents(Long.MinValue) - 1 + } + } + + it("should support the binary '*' operator, requiring a rounding mode.") { + implicit val mode = BigDecimal.RoundingMode.HALF_EVEN + (1.33 EUR) * (1.33 EUR) must equal(1.77 EUR) + } + + it("should throw error on overflow in the binary '*' operator.") { + a[MoneyOverflowException] must be thrownBy { + euroCents(Long.MaxValue / 2 + 1) * 2 + } + } + + it("should support the binary '/%' (divideAndRemainder) operator.") { + implicit val mode = BigDecimal.RoundingMode.HALF_EVEN + (1.33 EUR) /% 0.3 must equal(4.00 EUR, 0.13 EUR) + (1.33 EUR) /% 0.003 must equal(443.00 EUR, 0.00 EUR) + } + + it("should throw error on overflow in the binary '/%' (divideAndRemainder) operator.") { + a[MoneyOverflowException] must be thrownBy { + euroCents(Long.MaxValue) /% 0.5 + } + } + + it("should support getting the remainder of a division ('%').") { + implicit val mode = BigDecimal.RoundingMode.HALF_EVEN + (1.25 EUR).remainder(1.1) must equal(0.15 EUR) + (1.25 EUR) % 1.1 must equal(0.15 EUR) + } + + it("should not overflow when getting the remainder of a division ('%').") { + noException must be thrownBy { + euroCents(Long.MaxValue).remainder(0.5) + } + } + + it("should support partitioning an amount without losing or gaining money.") { + (0.05 EUR).partition(3, 7) must equal(Seq(0.02 EUR, 0.03 EUR)) + (10 EUR).partition(1, 2) must equal(Seq(3.34 EUR, 6.66 EUR)) + (10 EUR).partition(3, 1, 3) must equal(Seq(4.29 EUR, 1.43 EUR, 4.28 EUR)) + } + + it("should allow comparing money with the same currency.") { + ((1.10 EUR) > (1.00 EUR)) must be(true) + ((1.00 EUR) >= (1.00 EUR)) must be(true) + ((1.00 EUR) < (1.10 EUR)) must be(true) + ((1.00 EUR) <= (1.00 EUR)) must be(true) + } + + it("should support currencies with a scale of 0 (i.e. Japanese Yen)") { + (1 JPY) must equal(1 JPY) + } + + it("should be able to update the centAmount") { + (1.10 EUR).withCentAmount(170) must be(1.70 EUR) + (1.10 EUR).withCentAmount(1711) must be(17.11 EUR) + (1 JPY).withCentAmount(34) must be(34 JPY) + } + + it("should provide convenient toString") { + (1 JPY).toString must be("1 JPY") + (1.00 EUR).toString must be("1.00 EUR") + (0.10 EUR).toString must be("0.10 EUR") + (0.01 EUR).toString must be("0.01 EUR") + (0.00 EUR).toString must be("0.00 EUR") + (94.5 EUR).toString must be("94.50 EUR") + } + + it("should not fail on toString") { + forAll(DomainObjectsGen.money) { m => + m.toString + } + } + } +} diff --git a/util-3/src/test/scala/ScalaLoggingCompatiblitySpec.scala b/util-3/src/test/scala/ScalaLoggingCompatiblitySpec.scala new file mode 100644 index 00000000..35de2b23 --- /dev/null +++ b/util-3/src/test/scala/ScalaLoggingCompatiblitySpec.scala @@ -0,0 +1,18 @@ +import com.typesafe.scalalogging.Logger +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.must.Matchers + +class ScalaLoggingCompatiblitySpec extends AnyFunSpec with Matchers { + + describe("Ensure we skip ScalaLogging 3.9.5, because varargs will not compile under 3.9.5") { + // Github issue about the bug: https://github.com/lightbend-labs/scala-logging/issues/354 + // This test can be removed if it compiles with scala-logging versions bigger than 3.9.5 + object Log extends com.typesafe.scalalogging.StrictLogging { + val log: Logger = logger + } + val list: List[AnyRef] = List("log", "Some more") + + Log.log.warn("Message1", list*) + } + +} From 964f12e665ca77cfad98cbdd2191e91bacdaa6e3 Mon Sep 17 00:00:00 2001 From: Marcelo Gomes Date: Fri, 6 Dec 2024 17:39:39 +0100 Subject: [PATCH 050/142] [ENE-49] Remove fmpp from json-3. Remove json-derivation-3 module --- build.sbt | 7 ------- 1 file changed, 7 deletions(-) diff --git a/build.sbt b/build.sbt index 60a9b686..20953116 100644 --- a/build.sbt +++ b/build.sbt @@ -105,7 +105,6 @@ lazy val `sphere-json-3` = project .in(file("./json/json-3")) .settings(scalaVersion := scala3) .settings(standardSettings: _*) - .settings(Fmpp.settings: _*) .dependsOn(`sphere-util-3`) // Scala 2 modules @@ -126,12 +125,6 @@ lazy val `sphere-json-derivation` = project .settings(Fmpp.settings: _*) .dependsOn(`sphere-json-core`) -lazy val `sphere-json-derivation-scala-3` = project - .settings(scalaVersion := scala3) - .in(file("./json/json-derivation-scala-3")) - .settings(standardSettings: _*) - .dependsOn(`sphere-json-core`) - lazy val `sphere-json` = project .in(file("./json")) .settings(standardSettings: _*) From 43438a2583458815d4a96ba6e2e9dd836dfb12d8 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 30 Jan 2025 14:30:42 +0100 Subject: [PATCH 051/142] Add mongoTypeSwitch --- .../io/sphere/mongo/generic/generic.scala | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/generic.scala diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/generic.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/generic.scala new file mode 100644 index 00000000..a98ad26d --- /dev/null +++ b/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/generic.scala @@ -0,0 +1,71 @@ +package io.sphere.mongo.generic + +import com.mongodb.BasicDBObject +import io.sphere.mongo.format.MongoFormat +import org.bson.BSONObject + +import scala.annotation.tailrec +import scala.compiletime.{erasedValue, summonInline, error} + +case object generic { + inline def mongoTypeSwitch[SuperType, SubTypeTuple <: Tuple](): MongoFormat[SuperType] = + new MongoFormat[SuperType] { + failIfAnySubTypeIsNotAProduct[SubTypeTuple] + private val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] + private val typeHintMap = traitMetaData.subTypeTypeHints + private val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) + private val formatters: Vector[MongoFormat[Any]] = summonFormatters[SubTypeTuple]() + private val names = summonMetaData[SubTypeTuple]().map(_.name) + private val formattersByTypeName = names.zip(formatters).toMap + + override def toMongoValue(a: SuperType): Any = { + val originalTypeName = a.asInstanceOf[Product].productPrefix + val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) + val bson = + formattersByTypeName(originalTypeName).toMongoValue(a).asInstanceOf[BasicDBObject] + bson.put(traitMetaData.typeDiscriminator, typeName) + bson + } + + override def fromMongoValue(bson: Any): SuperType = + bson match { + case bson: BasicDBObject => + val typeName = bson.get(traitMetaData.typeDiscriminator).asInstanceOf[String] + val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) + formattersByTypeName(originalTypeName).fromMongoValue(bson).asInstanceOf[SuperType] + case x => + throw new Exception(s"BsonObject is expected for a Trait subtype, instead got $x") + } + } + + private def findTypeValue(dbo: BSONObject, typeField: String): Option[String] = + Option(dbo.get(typeField)).map(_.toString) + + inline private def failIfAnySubTypeIsNotAProduct[T <: Tuple]: Unit = + inline erasedValue[T] match { + case _: EmptyTuple => () + case _: (t *: ts) => + inline erasedValue[t] match { + case _: Product => failIfAnySubTypeIsNotAProduct[ts] + case _ => error("All types should be subtypes of Product") + } + } + + inline private def summonMetaData[T <: Tuple]( + acc: Vector[CaseClassMetaData] = Vector.empty): Vector[CaseClassMetaData] = + inline erasedValue[T] match { + case _: EmptyTuple => acc + case _: (t *: ts) => + summonMetaData[ts](acc :+ AnnotationReader.readCaseClassMetaData[t]) + } + + inline private def summonFormatters[T <: Tuple]( + acc: Vector[MongoFormat[Any]] = Vector.empty): Vector[MongoFormat[Any]] = + inline erasedValue[T] match { + case _: EmptyTuple => acc + case _: (t *: ts) => + val headFormatter = summonInline[MongoFormat[t]].asInstanceOf[MongoFormat[Any]] + summonFormatters[ts](acc :+ headFormatter) + } + +} From f9f0ef5f8eb755ada28294ce459f5efedfde7544 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 30 Jan 2025 14:31:33 +0100 Subject: [PATCH 052/142] Add mongoTypeSwitch --- .../mongo/generic/MongoTypeSwitchSpec.scala | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala new file mode 100644 index 00000000..59076963 --- /dev/null +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala @@ -0,0 +1,50 @@ +package io.sphere.mongo.generic + +import io.sphere.mongo.MongoUtils.* +import io.sphere.mongo.format.DefaultMongoFormats.given +import io.sphere.mongo.format.MongoFormat +import org.bson.BSONObject +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class MongoTypeSwitchSpec extends AnyWordSpec with Matchers { + + sealed trait A + case class B(int: Int) extends A + case class C(int: Int) extends A + @MongoTypeHint("D2") case class D(int: Int) extends A + + + "deriving TypedMongoFormat" must { + "derive a subset of a sealed trait" in { + val format = generic.mongoTypeSwitch[A, (B, C)]() + + val b = B(123) + val bson = format.toMongoValue(b) + + val b2 = format.fromMongoValue(bson) + + b2 must be(b) + + val c = C(2345345) + val bsonC = format.toMongoValue(c) + + val c2 = format.fromMongoValue(bsonC) + + c2 must be(c) + } + + "derive a subset of a sealed trait with a mongoKey" in { + val format = generic.mongoTypeSwitch[A, (B, D)]() + + val d = D(123) + val bson = format.toMongoValue(d).asInstanceOf[BSONObject] + val d2 = format.fromMongoValue(bson) + + bson.get("type") must be("D2") + d2 must be(d) + + } + + } +} From e2cf5eb61850f64cfc3bb9cf97134e493b7fd076 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 21 Feb 2025 10:13:23 +0100 Subject: [PATCH 053/142] Fix JSON derivation (moving the actual derivation to FromJSON and ToJSON) --- .../main/scala/io/sphere/json/FromJSON.scala | 129 +++++++-- .../src/main/scala/io/sphere/json/JSON.scala | 18 +- .../main/scala/io/sphere/json/ToJSON.scala | 105 ++++++-- .../json/generic/AnnotationReader.scala | 10 + .../io/sphere/json/generic/Derivation.scala | 250 +++++++++--------- .../sphere/json/generic/JsonTypeSwitch.scala | 78 ++++++ .../sphere/json/DeriveSingletonJSONSpec.scala | 1 - .../test/scala/io/sphere/json/JSONSpec.scala | 35 ++- .../io/sphere/json/TypesSwitchSpec.scala | 87 ------ .../json/generic/JsonTypeHintFieldSpec.scala | 2 - .../json/generic/JsonTypeSwitchSpec.scala | 107 ++++++++ .../io/sphere/mongo/generic/generic.scala | 1 - .../mongo/generic/MongoTypeSwitchSpec.scala | 6 +- 13 files changed, 546 insertions(+), 283 deletions(-) create mode 100644 json/json-3/src/main/scala/io/sphere/json/generic/JsonTypeSwitch.scala delete mode 100644 json/json-3/src/test/scala/io/sphere/json/TypesSwitchSpec.scala create mode 100644 json/json-3/src/test/scala/io/sphere/json/generic/JsonTypeSwitchSpec.scala diff --git a/json/json-3/src/main/scala/io/sphere/json/FromJSON.scala b/json/json-3/src/main/scala/io/sphere/json/FromJSON.scala index ed92252e..b6473466 100644 --- a/json/json-3/src/main/scala/io/sphere/json/FromJSON.scala +++ b/json/json-3/src/main/scala/io/sphere/json/FromJSON.scala @@ -3,17 +3,18 @@ package io.sphere.json import scala.util.control.NonFatal import scala.collection.mutable.ListBuffer import java.util.{Currency, Locale, UUID} - -import cats.data.NonEmptyList +import cats.data.{NonEmptyList, Validated} import cats.data.Validated.{Invalid, Valid} -import cats.syntax.apply._ -import cats.syntax.traverse._ +import cats.syntax.apply.* +import cats.syntax.traverse.* import io.sphere.json.field +import io.sphere.json.generic.{AnnotationReader, CaseClassMetaData, Field, TraitMetaData} import io.sphere.util.{BaseMoney, HighPrecisionMoney, LangTag, Money} -import org.json4s.JsonAST._ +import org.json4s.JsonAST.* +import org.json4s.DefaultReaders.StringReader +import org.json4s.{jvalue2monadic, jvalue2readerSyntax} import org.joda.time.format.ISODateTimeFormat -import scala.annotation.implicitNotFound import java.time import org.joda.time.DateTime import org.joda.time.DateTimeZone @@ -21,9 +22,10 @@ import org.joda.time.YearMonth import org.joda.time.LocalTime import org.joda.time.LocalDate +import scala.deriving.Mirror + /** Type class for types that can be read from JSON. */ -@implicitNotFound("Could not find an instance of FromJSON for ${A}") -trait FromJSON[@specialized A] extends Serializable { +trait FromJSON[A] extends Serializable { def read(jval: JValue): JValidation[A] final protected def fail(msg: String) = jsonParseError(msg) @@ -33,9 +35,105 @@ trait FromJSON[@specialized A] extends Serializable { object FromJSON extends FromJSONInstances { + inline def apply[A: JSON]: FromJSON[A] = summon[FromJSON[A]] + + inline given derived[A](using Mirror.Of[A]): FromJSON[A] = Derivation.derived[A] + + private def addField(jObject: JObject, field: Field, jValue: JValue): JValue = + jValue match { + case o: JObject => + if (field.embedded) JObject(jObject.obj ++ o.obj) + else JObject(jObject.obj :+ (field.fieldName -> o)) + case other => JObject(jObject.obj :+ (field.fieldName -> other)) + } + + private object Derivation { + + import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} + + inline def derived[A](using m: Mirror.Of[A]): FromJSON[A] = + inline m match { + case s: Mirror.SumOf[A] => deriveTrait(s) + case p: Mirror.ProductOf[A] => deriveCaseClass(p) + } + + inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): FromJSON[A] = + new FromJSON[A] { + private val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] + private val typeHintMap: Map[String, String] = traitMetaData.subtypes.collect { + case (name, classMeta) if classMeta.typeHint.isDefined => + name -> classMeta.typeHint.get + } + private val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) + private val fromJsons: Seq[FromJSON[Any]] = summonFromJsons[mirrorOfSum.MirroredElemTypes] + private val names: Seq[String] = + constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector + .asInstanceOf[Vector[String]] + private val jsonsByNames: Map[String, FromJSON[Any]] = names.zip(fromJsons).toMap + + override def read(jValue: JValue): JValidation[A] = + jValue match { + case jObject: JObject => + val typeName = (jObject \ traitMetaData.typeDiscriminator).as[String] + val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) + jsonsByNames(originalTypeName).read(jObject).map(_.asInstanceOf[A]) + case x => + Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$jValue'")) + } + } + + inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): FromJSON[A] = + new FromJSON[A] { + private val caseClassMetaData: CaseClassMetaData = AnnotationReader.readCaseClassMetaData[A] + private val fromJsons: Vector[FromJSON[Any]] = + summonFromJsons[mirrorOfProduct.MirroredElemTypes] + private val fieldsAndJsons: Vector[(Field, FromJSON[Any])] = + caseClassMetaData.fields.zip(fromJsons) + + private val fieldNames: Vector[String] = fieldsAndJsons.flatMap { (field, fromJson) => + if (field.embedded) fromJson.fields.toVector :+ field.name + else Vector(field.name) + } + + override val fields: Set[String] = fieldNames.toSet + + override def read(jValue: JValue): JValidation[A] = + jValue match { + case jObject: JObject => + for { + fieldsAsAList <- fieldsAndJsons + .map((field, fromJson) => readField(field, fromJson, jObject)) + .sequence + fieldsAsTuple = Tuple.fromArray(fieldsAsAList.toArray) + + } yield mirrorOfProduct.fromTuple( + fieldsAsTuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) + + case x => + Validated.invalidNel(JSONParseError(s"JSON object expected. $x")) + } + + private def readField( + field: Field, + fromJson: FromJSON[Any], + jObject: JObject): JValidation[Any] = + if (field.embedded) fromJson.read(jObject) + else io.sphere.json.field(field.fieldName, field.defaultArgument)(jObject)(fromJson) + + } + + inline private def summonFromJsons[T <: Tuple]: Vector[FromJSON[Any]] = + inline erasedValue[T] match { + case _: EmptyTuple => Vector.empty + case _: (t *: ts) => + summonInline[FromJSON[t]] + .asInstanceOf[FromJSON[Any]] +: summonFromJsons[ts] + } + } + private[FromJSON] val emptyFieldsSet: Set[String] = Set.empty - @inline def apply[A](implicit instance: FromJSON[A]): FromJSON[A] = instance + inline def apply[A](using instance: FromJSON[A]): FromJSON[A] = instance private val validNone = Valid(None) private val validNil = Valid(Nil) @@ -44,8 +142,7 @@ object FromJSON extends FromJSONInstances { private def validEmptyVector[A]: Valid[Vector[A]] = validEmptyAnyVector.asInstanceOf[Valid[Vector[A]]] - implicit def optionMapReader[@specialized A](implicit - c: FromJSON[A]): FromJSON[Option[Map[String, A]]] = + implicit def optionMapReader[A](implicit c: FromJSON[A]): FromJSON[Option[Map[String, A]]] = new FromJSON[Option[Map[String, A]]] { private val internalMapReader = mapReader[A] @@ -55,7 +152,7 @@ object FromJSON extends FromJSONInstances { } } - implicit def optionReader[@specialized A](implicit c: FromJSON[A]): FromJSON[Option[A]] = + given optionReader[A](using c: FromJSON[A]): FromJSON[Option[A]] = new FromJSON[Option[A]] { def read(jval: JValue): JValidation[Option[A]] = jval match { case JNothing | JNull | JObject(Nil) => validNone @@ -66,7 +163,7 @@ object FromJSON extends FromJSONInstances { override val fields: Set[String] = c.fields } - implicit def listReader[@specialized A](implicit r: FromJSON[A]): FromJSON[List[A]] = + implicit def listReader[A](implicit r: FromJSON[A]): FromJSON[List[A]] = new FromJSON[List[A]] { def read(jval: JValue): JValidation[List[A]] = jval match { @@ -96,12 +193,12 @@ object FromJSON extends FromJSONInstances { } } - implicit def seqReader[@specialized A](implicit r: FromJSON[A]): FromJSON[Seq[A]] = + implicit def seqReader[A](implicit r: FromJSON[A]): FromJSON[Seq[A]] = new FromJSON[Seq[A]] { def read(jval: JValue): JValidation[Seq[A]] = listReader(r).read(jval) } - implicit def setReader[@specialized A](implicit r: FromJSON[A]): FromJSON[Set[A]] = + implicit def setReader[A](implicit r: FromJSON[A]): FromJSON[Set[A]] = new FromJSON[Set[A]] { def read(jval: JValue): JValidation[Set[A]] = jval match { case JArray(l) => @@ -111,7 +208,7 @@ object FromJSON extends FromJSONInstances { } } - implicit def vectorReader[@specialized A](implicit r: FromJSON[A]): FromJSON[Vector[A]] = + implicit def vectorReader[A](implicit r: FromJSON[A]): FromJSON[Vector[A]] = new FromJSON[Vector[A]] { import scala.collection.immutable.VectorBuilder diff --git a/json/json-3/src/main/scala/io/sphere/json/JSON.scala b/json/json-3/src/main/scala/io/sphere/json/JSON.scala index 3e2366a2..c73f0f60 100644 --- a/json/json-3/src/main/scala/io/sphere/json/JSON.scala +++ b/json/json-3/src/main/scala/io/sphere/json/JSON.scala @@ -1,22 +1,26 @@ package io.sphere.json +import cats.implicits.* import org.json4s.JsonAST.JValue -import scala.annotation.implicitNotFound +import scala.deriving.Mirror -@implicitNotFound("Could not find an instance of JSON for ${A}") trait JSON[A] extends FromJSON[A] with ToJSON[A] -object JSON extends JSONInstances with JSONLowPriorityImplicits { - @inline def apply[A](implicit instance: JSON[A]): JSON[A] = instance -} +inline def deriveJSON[A](using Mirror.Of[A]): JSON[A] = JSON.derived + +object JSON extends JSONInstances { + inline def apply[A: JSON]: JSON[A] = summon[JSON[A]] -trait JSONLowPriorityImplicits { - implicit def fromJSONAndToJSON[A](implicit fromJSON: FromJSON[A], toJSON: ToJSON[A]): JSON[A] = + inline given derived[A](using fromJSON: FromJSON[A], toJSON: ToJSON[A]): JSON[A] = new JSON[A] { override def read(jval: JValue): JValidation[A] = fromJSON.read(jval) + override def write(value: A): JValue = toJSON.write(value) + + override val fields: Set[String] = fromJSON.fields } + } class JSONException(msg: String) extends RuntimeException(msg) diff --git a/json/json-3/src/main/scala/io/sphere/json/ToJSON.scala b/json/json-3/src/main/scala/io/sphere/json/ToJSON.scala index 8cf778ea..b4f708b9 100644 --- a/json/json-3/src/main/scala/io/sphere/json/ToJSON.scala +++ b/json/json-3/src/main/scala/io/sphere/json/ToJSON.scala @@ -1,23 +1,18 @@ package io.sphere.json import cats.data.NonEmptyList -import java.util.{Currency, Locale, UUID} - +import io.sphere.json.generic.{AnnotationReader, CaseClassMetaData, Field, TraitMetaData} import io.sphere.util.{BaseMoney, HighPrecisionMoney, Money} -import org.json4s.JsonAST._ -import org.joda.time.DateTime -import org.joda.time.DateTimeZone -import org.joda.time.LocalTime -import org.joda.time.LocalDate -import org.joda.time.YearMonth +import org.joda.time.* import org.joda.time.format.ISODateTimeFormat +import org.json4s.JsonAST.* -import scala.annotation.implicitNotFound import java.time +import java.util.{Currency, Locale, UUID} +import scala.deriving.Mirror /** Type class for types that can be written to JSON. */ -@implicitNotFound("Could not find an instance of ToJSON for ${A}") -trait ToJSON[@specialized A] extends Serializable { +trait ToJSON[A] extends Serializable { def write(value: A): JValue } @@ -25,10 +20,82 @@ class JSONWriteException(msg: String) extends JSONException(msg) object ToJSON extends ToJSONInstances { + inline def apply[A: JSON]: ToJSON[A] = summon[ToJSON[A]] + + inline given derived[A](using Mirror.Of[A]): ToJSON[A] = Derivation.derived[A] + + private def addField(jObject: JObject, field: Field, jValue: JValue): JValue = + jValue match { + case o: JObject => + if (field.embedded) JObject(jObject.obj ++ o.obj) + else JObject(jObject.obj :+ (field.fieldName -> o)) + case other => JObject(jObject.obj :+ (field.fieldName -> other)) + } + + private object Derivation { + + import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} + + inline def derived[A](using m: Mirror.Of[A]): ToJSON[A] = + inline m match { + case s: Mirror.SumOf[A] => deriveTrait(s) + case p: Mirror.ProductOf[A] => deriveCaseClass(p) + } + + inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): ToJSON[A] = + new ToJSON[A] { + private val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] + private val typeHintMap: Map[String, String] = traitMetaData.subtypes.collect { + case (name, classMeta) if classMeta.typeHint.isDefined => + name -> classMeta.typeHint.get + } + private val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) + private val jsons: Seq[ToJSON[Any]] = summonToJson[mirrorOfSum.MirroredElemTypes] + private val names: Seq[String] = + constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector + .asInstanceOf[Vector[String]] + private val jsonsByNames: Map[String, ToJSON[Any]] = names.zip(jsons).toMap + + override def write(value: A): JValue = { + // we never get a trait here, only classes, it's safe to assume Product + val originalTypeName = value.asInstanceOf[Product].productPrefix + val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) + val json = jsonsByNames(originalTypeName).write(value).asInstanceOf[JObject] + val typeDiscriminator = traitMetaData.typeDiscriminator -> JString(typeName) + JObject(typeDiscriminator :: json.obj) + } + + } + + inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): ToJSON[A] = + new ToJSON[A] { + private val caseClassMetaData: CaseClassMetaData = AnnotationReader.readCaseClassMetaData[A] + private val toJsons: Vector[ToJSON[Any]] = summonToJson[mirrorOfProduct.MirroredElemTypes] + + override def write(value: A): JValue = { + val caseClassFields = value.asInstanceOf[Product].productIterator + toJsons + .zip(caseClassFields) + .zip(caseClassMetaData.fields) + .foldLeft[JValue](JObject()) { case (jObject, ((toJson, fieldValue), field)) => + addField(jObject.asInstanceOf[JObject], field, toJson.write(fieldValue)) + } + } + } + + inline private def summonToJson[T <: Tuple]: Vector[ToJSON[Any]] = + inline erasedValue[T] match { + case _: EmptyTuple => Vector.empty + case _: (t *: ts) => + summonInline[ToJSON[t]] + .asInstanceOf[ToJSON[Any]] +: summonToJson[ts] + } + } + private val emptyJArray = JArray(Nil) private val emptyJObject = JObject(Nil) - @inline def apply[A](implicit instance: ToJSON[A]): ToJSON[A] = instance + inline def apply[A](implicit instance: ToJSON[A]): ToJSON[A] = instance /** construct an instance from a function */ @@ -36,7 +103,7 @@ object ToJSON extends ToJSONInstances { override def write(value: T): JValue = toJson(value) } - implicit def optionWriter[@specialized A](implicit c: ToJSON[A]): ToJSON[Option[A]] = + given optionWriter[A](using c: ToJSON[A]): ToJSON[Option[A]] = new ToJSON[Option[A]] { def write(opt: Option[A]): JValue = opt match { case Some(a) => c.write(a) @@ -44,7 +111,7 @@ object ToJSON extends ToJSONInstances { } } - implicit def listWriter[@specialized A](implicit w: ToJSON[A]): ToJSON[List[A]] = + implicit def listWriter[A](implicit w: ToJSON[A]): ToJSON[List[A]] = new ToJSON[List[A]] { def write(l: List[A]): JValue = if (l.isEmpty) emptyJArray @@ -56,21 +123,21 @@ object ToJSON extends ToJSONInstances { def write(l: NonEmptyList[A]): JValue = JArray(l.toList.map(w.write)) } - implicit def seqWriter[@specialized A](implicit w: ToJSON[A]): ToJSON[Seq[A]] = + implicit def seqWriter[A](implicit w: ToJSON[A]): ToJSON[Seq[A]] = new ToJSON[Seq[A]] { def write(s: Seq[A]): JValue = if (s.isEmpty) emptyJArray else JArray(s.iterator.map(w.write).toList) } - implicit def setWriter[@specialized A](implicit w: ToJSON[A]): ToJSON[Set[A]] = + implicit def setWriter[A](implicit w: ToJSON[A]): ToJSON[Set[A]] = new ToJSON[Set[A]] { def write(s: Set[A]): JValue = if (s.isEmpty) emptyJArray else JArray(s.iterator.map(w.write).toList) } - implicit def vectorWriter[@specialized A](implicit w: ToJSON[A]): ToJSON[Vector[A]] = + implicit def vectorWriter[A](implicit w: ToJSON[A]): ToJSON[Vector[A]] = new ToJSON[Vector[A]] { def write(v: Vector[A]): JValue = if (v.isEmpty) emptyJArray @@ -119,7 +186,7 @@ object ToJSON extends ToJSONInstances { } implicit val moneyWriter: ToJSON[Money] = new ToJSON[Money] { - import Money._ + import Money.* def write(m: Money): JValue = JObject( JField(BaseMoney.TypeField, toJValue(m.`type`)) :: @@ -132,7 +199,7 @@ object ToJSON extends ToJSONInstances { implicit val highPrecisionMoneyWriter: ToJSON[HighPrecisionMoney] = new ToJSON[HighPrecisionMoney] { - import HighPrecisionMoney._ + import HighPrecisionMoney.* def write(m: HighPrecisionMoney): JValue = JObject( JField(BaseMoney.TypeField, toJValue(m.`type`)) :: JField(CurrencyCodeField, toJValue(m.currency)) :: diff --git a/json/json-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala b/json/json-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala index 69c64576..67540fa1 100644 --- a/json/json-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala +++ b/json/json-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala @@ -25,12 +25,22 @@ case class CaseClassMetaData( typeHintRaw.map(_.value).filterNot(_.toList.forall(_ == ' ')) } +/** This class also works for case classes not only traits, in case of case classes only the `top` + * field would be populated + */ case class TraitMetaData( top: CaseClassMetaData, typeHintFieldRaw: Option[JSONTypeHintField], subtypes: Map[String, CaseClassMetaData] ) { + def isTrait: Boolean = subtypes.nonEmpty + val typeDiscriminator: String = typeHintFieldRaw.map(_.value).getOrElse("type") + + val subTypeTypeHints: Map[String, String] = subtypes.collect { + case (name, classMeta) if classMeta.typeHint.isDefined => + name -> classMeta.typeHint.get + } } class AnnotationReader(using q: Quotes) { diff --git a/json/json-3/src/main/scala/io/sphere/json/generic/Derivation.scala b/json/json-3/src/main/scala/io/sphere/json/generic/Derivation.scala index 67a26666..805654ea 100644 --- a/json/json-3/src/main/scala/io/sphere/json/generic/Derivation.scala +++ b/json/json-3/src/main/scala/io/sphere/json/generic/Derivation.scala @@ -1,126 +1,124 @@ -package io.sphere.json.generic - -import cats.data.Validated -import cats.implicits.* -import io.sphere.json.{JSON, JSONParseError, JValidation} -import org.json4s.DefaultJsonFormats.given -import org.json4s.JsonAST.JValue -import org.json4s.{DefaultJsonFormats, JObject, JString, jvalue2monadic, jvalue2readerSyntax} - -import scala.deriving.Mirror - -inline def deriveJSON[A](using Mirror.Of[A]): JSON[A] = JSON.derived - -object JSON { - private val emptyFieldsSet: Vector[String] = Vector.empty - - inline def apply[A: JSON]: JSON[A] = summon[JSON[A]] - inline given derived[A](using Mirror.Of[A]): JSON[A] = Derivation.derived[A] - - private def addField(jObject: JObject, field: Field, jValue: JValue): JValue = - jValue match { - case o: JObject => - if (field.embedded) JObject(jObject.obj ++ o.obj) - else JObject(jObject.obj :+ (field.fieldName -> o)) - case other => JObject(jObject.obj :+ (field.fieldName -> other)) - } - - private object Derivation { - - import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} - - inline def derived[A](using m: Mirror.Of[A]): JSON[A] = - inline m match { - case s: Mirror.SumOf[A] => deriveTrait(s) - case p: Mirror.ProductOf[A] => deriveCaseClass(p) - } - - inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): JSON[A] = - new JSON[A] { - private val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] - private val typeHintMap: Map[String, String] = traitMetaData.subtypes.collect { - case (name, classMeta) if classMeta.typeHint.isDefined => - name -> classMeta.typeHint.get - } - private val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) - private val jsons: Seq[JSON[Any]] = summonFormatters[mirrorOfSum.MirroredElemTypes] - private val names: Seq[String] = - constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector - .asInstanceOf[Vector[String]] - private val jsonsByNames: Map[String, JSON[Any]] = names.zip(jsons).toMap - - override def read(jValue: JValue): JValidation[A] = - jValue match { - case jObject: JObject => - val typeName = (jObject \ traitMetaData.typeDiscriminator).as[String] - val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - jsonsByNames(originalTypeName).read(jObject).map(_.asInstanceOf[A]) - case x => - Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$jValue'")) - } - - override def write(value: A): JValue = { - // we never get a trait here, only classes, it's safe to assume Product - val originalTypeName = value.asInstanceOf[Product].productPrefix - val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) - val json = jsonsByNames(originalTypeName).write(value).asInstanceOf[JObject] - val typeDiscriminator = traitMetaData.typeDiscriminator -> JString(typeName) - JObject(typeDiscriminator :: json.obj) - } - - } - - inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): JSON[A] = - new JSON[A] { - private val caseClassMetaData: CaseClassMetaData = AnnotationReader.readCaseClassMetaData[A] - private val jsons: Vector[JSON[Any]] = summonFormatters[mirrorOfProduct.MirroredElemTypes] - private val fieldsAndJsons: Vector[(Field, JSON[Any])] = caseClassMetaData.fields.zip(jsons) - - private val fieldNames: Vector[String] = fieldsAndJsons.flatMap { (field, json) => - if (field.embedded) json.fields.toVector :+ field.name - else Vector(field.name) - } - - override val fields: Set[String] = fieldNames.toSet - - override def write(value: A): JValue = { - val caseClassFields = value.asInstanceOf[Product].productIterator - jsons - .zip(caseClassFields) - .zip(caseClassMetaData.fields) - .foldLeft[JValue](JObject()) { case (jObject, ((json, fieldValue), field)) => - addField(jObject.asInstanceOf[JObject], field, json.write(fieldValue)) - } - } - - override def read(jValue: JValue): JValidation[A] = - jValue match { - case jObject: JObject => - for { - fieldsAsAList <- fieldsAndJsons - .map((field, format) => readField(field, format, jObject)) - .sequence - fieldsAsTuple = Tuple.fromArray(fieldsAsAList.toArray) - - } yield mirrorOfProduct.fromTuple( - fieldsAsTuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) - - case x => - Validated.invalidNel(JSONParseError(s"JSON object expected. $x")) - } - - private def readField(field: Field, json: JSON[Any], jObject: JObject): JValidation[Any] = - if (field.embedded) json.read(jObject) - else io.sphere.json.field(field.fieldName, field.defaultArgument)(jObject)(json) - - } - - inline private def summonFormatters[T <: Tuple]: Vector[JSON[Any]] = - inline erasedValue[T] match { - case _: EmptyTuple => Vector.empty - case _: (t *: ts) => - summonInline[JSON[t]] - .asInstanceOf[JSON[Any]] +: summonFormatters[ts] - } - } -} +//package io.sphere.json.generic +// +//import cats.data.Validated +//import cats.implicits.* +//import io.sphere.json.{JSON, JSONParseError, JValidation} +//import org.json4s.DefaultJsonFormats.given +//import org.json4s.JsonAST.JValue +//import org.json4s.{DefaultJsonFormats, JObject, JString, jvalue2monadic, jvalue2readerSyntax} +// +//import scala.deriving.Mirror +// +//inline def deriveJSON[A](using Mirror.Of[A]): JSON[A] = JSON.derived +// +//object JSON { +// inline def apply[A: JSON]: JSON[A] = summon[JSON[A]] +// inline given derived[A](using Mirror.Of[A]): JSON[A] = Derivation.derived[A] +// +// private def addField(jObject: JObject, field: Field, jValue: JValue): JValue = +// jValue match { +// case o: JObject => +// if (field.embedded) JObject(jObject.obj ++ o.obj) +// else JObject(jObject.obj :+ (field.fieldName -> o)) +// case other => JObject(jObject.obj :+ (field.fieldName -> other)) +// } +// +// private object Derivation { +// +// import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} +// +// inline def derived[A](using m: Mirror.Of[A]): JSON[A] = +// inline m match { +// case s: Mirror.SumOf[A] => deriveTrait(s) +// case p: Mirror.ProductOf[A] => deriveCaseClass(p) +// } +// +// inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): JSON[A] = +// new JSON[A] { +// private val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] +// private val typeHintMap: Map[String, String] = traitMetaData.subtypes.collect { +// case (name, classMeta) if classMeta.typeHint.isDefined => +// name -> classMeta.typeHint.get +// } +// private val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) +// private val jsons: Seq[JSON[Any]] = summonFormatters[mirrorOfSum.MirroredElemTypes] +// private val names: Seq[String] = +// constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector +// .asInstanceOf[Vector[String]] +// private val jsonsByNames: Map[String, JSON[Any]] = names.zip(jsons).toMap +// +// override def read(jValue: JValue): JValidation[A] = +// jValue match { +// case jObject: JObject => +// val typeName = (jObject \ traitMetaData.typeDiscriminator).as[String] +// val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) +// jsonsByNames(originalTypeName).read(jObject).map(_.asInstanceOf[A]) +// case x => +// Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$jValue'")) +// } +// +// override def write(value: A): JValue = { +// // we never get a trait here, only classes, it's safe to assume Product +// val originalTypeName = value.asInstanceOf[Product].productPrefix +// val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) +// val json = jsonsByNames(originalTypeName).write(value).asInstanceOf[JObject] +// val typeDiscriminator = traitMetaData.typeDiscriminator -> JString(typeName) +// JObject(typeDiscriminator :: json.obj) +// } +// +// } +// +// inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): JSON[A] = +// new JSON[A] { +// private val caseClassMetaData: CaseClassMetaData = AnnotationReader.readCaseClassMetaData[A] +// private val jsons: Vector[JSON[Any]] = summonFormatters[mirrorOfProduct.MirroredElemTypes] +// private val fieldsAndJsons: Vector[(Field, JSON[Any])] = caseClassMetaData.fields.zip(jsons) +// +// private val fieldNames: Vector[String] = fieldsAndJsons.flatMap { (field, json) => +// if (field.embedded) json.fields.toVector :+ field.name +// else Vector(field.name) +// } +// +// override val fields: Set[String] = fieldNames.toSet +// +// override def write(value: A): JValue = { +// val caseClassFields = value.asInstanceOf[Product].productIterator +// jsons +// .zip(caseClassFields) +// .zip(caseClassMetaData.fields) +// .foldLeft[JValue](JObject()) { case (jObject, ((json, fieldValue), field)) => +// addField(jObject.asInstanceOf[JObject], field, json.write(fieldValue)) +// } +// } +// +// override def read(jValue: JValue): JValidation[A] = +// jValue match { +// case jObject: JObject => +// for { +// fieldsAsAList <- fieldsAndJsons +// .map((field, format) => readField(field, format, jObject)) +// .sequence +// fieldsAsTuple = Tuple.fromArray(fieldsAsAList.toArray) +// +// } yield mirrorOfProduct.fromTuple( +// fieldsAsTuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) +// +// case x => +// Validated.invalidNel(JSONParseError(s"JSON object expected. $x")) +// } +// +// private def readField(field: Field, json: JSON[Any], jObject: JObject): JValidation[Any] = +// if (field.embedded) json.read(jObject) +// else io.sphere.json.field(field.fieldName, field.defaultArgument)(jObject)(json) +// +// } +// +// inline private def summonFormatters[T <: Tuple]: Vector[JSON[Any]] = +// inline erasedValue[T] match { +// case _: EmptyTuple => Vector.empty +// case _: (t *: ts) => +// summonInline[JSON[t]] +// .asInstanceOf[JSON[Any]] +: summonFormatters[ts] +// } +// } +//} diff --git a/json/json-3/src/main/scala/io/sphere/json/generic/JsonTypeSwitch.scala b/json/json-3/src/main/scala/io/sphere/json/generic/JsonTypeSwitch.scala new file mode 100644 index 00000000..385f7e90 --- /dev/null +++ b/json/json-3/src/main/scala/io/sphere/json/generic/JsonTypeSwitch.scala @@ -0,0 +1,78 @@ +package io.sphere.json.generic + +import cats.data.Validated +import io.sphere.json.{JSON, JSONParseError, JValidation} +import org.json4s.DefaultJsonFormats.given +import org.json4s.{JObject, JString, jvalue2monadic, jvalue2readerSyntax} +import org.json4s.JsonAST.JValue + +import scala.deriving.Mirror + +object JsonTypeSwitch { + import scala.compiletime.{erasedValue, error, summonInline} + + inline def jsonTypeSwitch[SuperType, SubTypeTuple <: Tuple](): JSON[SuperType] = + new JSON[SuperType] { + private val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] + private val typeHintMap = traitMetaData.subTypeTypeHints + private val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) + private val formattersAndMetaData: Vector[(TraitMetaData, JSON[Any])] = + summonFormatters[SubTypeTuple]() + + // Separate Trait formatters from CaseClass formatters, so we can avoid adding the typeDiscriminator twice + private val (traitFormatterList, caseClassFormatterList) = + formattersAndMetaData.partitionMap { (meta, formatter) => + if (meta.isTrait) + Left(meta.subtypes.map(_._2.name -> formatter)) + else + Right(meta.top.name -> formatter) + } + val traitFormatters = traitFormatterList.flatten.toMap + val caseClassFormatters = caseClassFormatterList.toMap + val allFormattersByTypeName = traitFormatters ++ caseClassFormatters + + override def write(a: SuperType): JValue = { + val originalTypeName = a.asInstanceOf[Product].productPrefix + val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) + val traitFormatterOpt = traitFormatters.get(originalTypeName) + traitFormatterOpt + .map(_.write(a).asInstanceOf[JObject]) + .getOrElse { + val json = caseClassFormatters(originalTypeName).write(a).asInstanceOf[JObject] + val typeDiscriminator = traitMetaData.typeDiscriminator -> JString(typeName) + JObject(typeDiscriminator :: json.obj) + } + } + + override def read(jValue: JValue): JValidation[SuperType] = + jValue match { + case jObject: JObject => + val typeName = (jObject \ traitMetaData.typeDiscriminator).as[String] + val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) + allFormattersByTypeName(originalTypeName).read(jObject).map(_.asInstanceOf[SuperType]) + case x => + Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$jValue'")) + } + } + + inline private def failIfAnySubTypeIsNotAProduct[T <: Tuple]: Unit = + inline erasedValue[T] match { + case _: EmptyTuple => () + case _: (t *: ts) => + inline erasedValue[t] match { + case _: Product => failIfAnySubTypeIsNotAProduct[ts] + case _ => error("All types should be subtypes of Product") + } + } + + inline private def summonFormatters[T <: Tuple]( + acc: Vector[(TraitMetaData, JSON[Any])] = Vector.empty): Vector[(TraitMetaData, JSON[Any])] = + inline erasedValue[T] match { + case _: EmptyTuple => acc + case _: (t *: ts) => + val traitMetaData = AnnotationReader.readTraitMetaData[t] + val headFormatter = summonInline[JSON[t]].asInstanceOf[JSON[Any]] + summonFormatters[ts](acc :+ (traitMetaData -> headFormatter)) + } + +} diff --git a/json/json-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala b/json/json-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala index dc334403..43468045 100644 --- a/json/json-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala +++ b/json/json-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala @@ -154,7 +154,6 @@ object PictureSize { sealed trait Access object Access { // only one sub-type - import JSON.derived case class Authorized(project: String) extends Access given JSON[Access] = deriveJSON diff --git a/json/json-3/src/test/scala/io/sphere/json/JSONSpec.scala b/json/json-3/src/test/scala/io/sphere/json/JSONSpec.scala index d3c86330..cb12c5e2 100644 --- a/json/json-3/src/test/scala/io/sphere/json/JSONSpec.scala +++ b/json/json-3/src/test/scala/io/sphere/json/JSONSpec.scala @@ -27,7 +27,7 @@ object JSONSpec { case class GenericA[A](a: A) extends GenericBase[A] case class GenericB[A](a: A) extends GenericBase[A] - object Singleton + case object Singleton sealed abstract class SingletonEnum case object SingletonA extends SingletonEnum @@ -162,7 +162,6 @@ class JSONSpec extends AnyFunSpec with Matchers { } it("must provide derived JSON instances for sum types") { - import io.sphere.json.generic.JSON.derived given JSON[Animal] = deriveJSON List(Bird("Peewee"), Dog("Hasso"), Cat("Felidae")).foreach { animal => fromJSON[Animal](toJSON(animal)) must equal(Valid(animal)) @@ -181,23 +180,21 @@ class JSONSpec extends AnyFunSpec with Matchers { fromJSON[GenericA[String]](toJSON(a)) must equal(Valid(a)) } -// it("must provide derived instances for singleton objects") { -// import io.sphere.json.generic.JSON.derived -// implicit val singletonJSON: JSON[JSONSpec.Singleton.type] = deriveJSON[JSONSpec.Singleton.type] -// -// val json = s"""[${toJSON(Singleton)}]""" -// withClue(json) { -// fromJSON[Seq[Singleton.type]](json) must equal(Valid(Seq(Singleton))) -// } -// -// implicit val singleEnumJSON: JSON[SingletonEnum] = deriveJSON[SingletonEnum] -// List(SingletonA, SingletonB, SingletonC).foreach { s => -// fromJSON[SingletonEnum](toJSON(s)) must equal(Valid(s)) -// } -// } + it("must provide derived instances for singleton objects") { + implicit val singletonJSON: JSON[JSONSpec.Singleton.type] = deriveJSON[JSONSpec.Singleton.type] + + val json = s"""[${toJSON(Singleton)}]""" + withClue(json) { + fromJSON[Seq[Singleton.type]](json) must equal(Valid(Seq(Singleton))) + } + + implicit val singleEnumJSON: JSON[SingletonEnum] = deriveJSON[SingletonEnum] + List(SingletonA, SingletonB, SingletonC).foreach { s => + fromJSON[SingletonEnum](toJSON(s)) must equal(Valid(s)) + } + } it("must provide derived instances for sum types with a mix of case class / object") { - import io.sphere.json.generic.JSON.derived given JSON[Mixed] = deriveJSON List(SingletonMixed, RecordMixed(1)).foreach { m => fromJSON[Mixed](toJSON(m)) must equal(Valid(m)) @@ -390,13 +387,11 @@ case class TestSubjectConcrete4(c4: String) extends TestSubjectCategoryB object TestSubjectCategoryA { - import io.sphere.json.generic.JSON.derived val json: JSON[TestSubjectCategoryA] = deriveJSON[TestSubjectCategoryA] } object TestSubjectCategoryB { - import io.sphere.json.generic.JSON.derived val json: JSON[TestSubjectCategoryB] = deriveJSON[TestSubjectCategoryB] } @@ -405,6 +400,6 @@ object TestSubjectCategoryB { // implicit val jsonA = TestSubjectCategoryA.json // implicit val jsonB = TestSubjectCategoryB.json // -// jsonTypeSwitch[TestSubjectBase, TestSubjectCategoryA, TestSubjectCategoryB](Nil) +// jsonTypeSwitch[TestSubjectBase, (TestSubjectCategoryA, TestSubjectCategoryB)]() // } //} diff --git a/json/json-3/src/test/scala/io/sphere/json/TypesSwitchSpec.scala b/json/json-3/src/test/scala/io/sphere/json/TypesSwitchSpec.scala deleted file mode 100644 index 88f49334..00000000 --- a/json/json-3/src/test/scala/io/sphere/json/TypesSwitchSpec.scala +++ /dev/null @@ -1,87 +0,0 @@ -//package io.sphere.json -// -//import io.sphere.json.generic.{TypeSelectorContainer, deriveJSON, jsonTypeSwitch} -//import org.json4s._ -//import org.scalatest.matchers.must.Matchers -//import org.scalatest.wordspec.AnyWordSpec -// -//class TypesSwitchSpec extends AnyWordSpec with Matchers { -// import TypesSwitchSpec._ -// -// "jsonTypeSwitch" must { -// "combine different sum types tree" in { -// val m: Seq[Message] = List( -// TypeA.ClassA1(23), -// TypeA.ClassA2("world"), -// TypeB.ClassB1(valid = false), -// TypeB.ClassB2(Seq("a23", "c62"))) -// -// val jsons = m.map(Message.json.write) -// jsons must be( -// List( -// JObject("number" -> JLong(23), "type" -> JString("ClassA1")), -// JObject("name" -> JString("world"), "type" -> JString("ClassA2")), -// JObject("valid" -> JBool(false), "type" -> JString("ClassB1")), -// JObject( -// "references" -> JArray(List(JString("a23"), JString("c62"))), -// "type" -> JString("ClassB2")) -// )) -// -// val messages = jsons.map(Message.json.read).map(_.toOption.get) -// messages must be(m) -// } -// } -// -// "TypeSelectorContainer" must { -// "have information about type value discriminators" in { -// val selectors = Message.json.typeSelectors -// selectors.map(_.typeValue) must contain.allOf( -// "ClassA1", -// "ClassA2", -// "TypeA", -// "ClassB1", -// "ClassB2", -// "TypeB") -// -// // I don't think it's useful to allow different type fields. How is it possible to deserialize one json -// // if different type fields are used? -// selectors.map(_.typeField) must be(List("type", "type", "type", "type", "type", "type")) -// -// selectors.map(_.clazz.getName) must contain.allOf( -// "io.sphere.json.TypesSwitchSpec$TypeA$ClassA1", -// "io.sphere.json.TypesSwitchSpec$TypeA$ClassA2", -// "io.sphere.json.TypesSwitchSpec$TypeA", -// "io.sphere.json.TypesSwitchSpec$TypeB$ClassB1", -// "io.sphere.json.TypesSwitchSpec$TypeB$ClassB2", -// "io.sphere.json.TypesSwitchSpec$TypeB" -// ) -// } -// } -// -//} -// -//object TypesSwitchSpec { -// -// trait Message -// object Message { -// // this can be dangerous is the same class name is used in both sum types -// // ex if we define TypeA.Class1 && TypeB.Class1 -// // as both will use the same type value discriminator -// implicit val json: JSON[Message] with TypeSelectorContainer = -// jsonTypeSwitch[Message, TypeA, TypeB](Nil) -// } -// -// sealed trait TypeA extends Message -// object TypeA { -// case class ClassA1(number: Int) extends TypeA -// case class ClassA2(name: String) extends TypeA -// implicit val json: JSON[TypeA] = deriveJSON[TypeA] -// } -// -// sealed trait TypeB extends Message -// object TypeB { -// case class ClassB1(valid: Boolean) extends TypeB -// case class ClassB2(references: Seq[String]) extends TypeB -// implicit val json: JSON[TypeB] = deriveJSON[TypeB] -// } -//} diff --git a/json/json-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala b/json/json-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala index 4658d1d0..831176bc 100644 --- a/json/json-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala +++ b/json/json-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala @@ -56,8 +56,6 @@ object JsonTypeHintFieldSpec { case class UserWithPicture(userId: String, pictureSize: PictureSize, pictureUrl: String) object UserWithPicture { - import io.sphere.json.generic.JSON.given - import io.sphere.json.generic.deriveJSON given JSON[UserWithPicture] = deriveJSON[UserWithPicture] } } diff --git a/json/json-3/src/test/scala/io/sphere/json/generic/JsonTypeSwitchSpec.scala b/json/json-3/src/test/scala/io/sphere/json/generic/JsonTypeSwitchSpec.scala new file mode 100644 index 00000000..1ad3d5cc --- /dev/null +++ b/json/json-3/src/test/scala/io/sphere/json/generic/JsonTypeSwitchSpec.scala @@ -0,0 +1,107 @@ +package io.sphere.json.generic + +import cats.data.Validated.Valid +import io.sphere.json.{JSON, deriveJSON} +import io.sphere.json.generic.JsonTypeSwitch.jsonTypeSwitch +import org.json4s.JsonAST.JObject +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec +import org.json4s.* +import org.json4s.DefaultReaders.StringReader + +class JsonTypeSwitchSpec extends AnyWordSpec with Matchers { + + "jsonTypeSwitch" must { + import JsonTypeSwitchSpec.* + + "derive a subset of a sealed trait" in { + given JSON[B] = deriveJSON[B] + val format = jsonTypeSwitch[A, (B, C)]() + + val b = B(123) + val jsonB = format.write(b) + + val b2 = format.read(jsonB).getOrElse(null) + + b2 must be(b) + + val c = C(2345345) + val jsonC = format.write(c) + + val c2 = format.read(jsonC).getOrElse(null) + + c2 must be(c) + } + + "derive a subset of a sealed trait with a mongoKey" in { + val format = jsonTypeSwitch[A, (B, D)]() + + val d = D(123) + val json = format.write(d) + val d2 = format.read(json) + + (json \ "type").as[String] must be("D2") + d2 must be(Valid(d)) + + } + + "combine different sum types tree" in { + val m: Seq[Message] = List( + TypeA.ClassA1(23), + TypeA.ClassA2("world"), + TypeB.ClassB1(valid = false), + TypeB.ClassB2(Seq("a23", "c62"))) + + val jsons = m.map(Message.json.write) + jsons must be( + List( + JObject("number" -> JLong(23), "type" -> JString("ClassA1")), + JObject("name" -> JString("world"), "type" -> JString("ClassA2")), + JObject("valid" -> JBool(false), "type" -> JString("ClassB1")), + JObject( + "references" -> JArray(List(JString("a23"), JString("c62"))), + "type" -> JString("ClassB2")) + )) + + val messages = jsons.map(Message.json.read).map(_.toOption.get) + messages must be(m) + } + } + +} + +object JsonTypeSwitchSpec { + sealed trait A + case class B(int: Int) extends A + case class C(int: Int) extends A + @JSONTypeHint("D2") case class D(int: Int) extends A + + trait Message + + object Message { + // this can be dangerous is the same class name is used in both sum types + // ex if we define TypeA.Class1 && TypeB.Class1 + // as both will use the same type value discriminator + implicit val json: JSON[Message] = JsonTypeSwitch.jsonTypeSwitch[Message, (TypeA, TypeB)]() + } + + sealed trait TypeA extends Message + + object TypeA { + case class ClassA1(number: Int) extends TypeA + + case class ClassA2(name: String) extends TypeA + + implicit val json: JSON[TypeA] = deriveJSON[TypeA] + } + + sealed trait TypeB extends Message + + object TypeB { + case class ClassB1(valid: Boolean) extends TypeB + + case class ClassB2(references: Seq[String]) extends TypeB + + implicit val json: JSON[TypeB] = deriveJSON[TypeB] + } +} diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/generic.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/generic.scala index a98ad26d..9eba298b 100644 --- a/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/generic.scala +++ b/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/generic.scala @@ -4,7 +4,6 @@ import com.mongodb.BasicDBObject import io.sphere.mongo.format.MongoFormat import org.bson.BSONObject -import scala.annotation.tailrec import scala.compiletime.{erasedValue, summonInline, error} case object generic { diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala index 59076963..0b6d6090 100644 --- a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala @@ -13,9 +13,8 @@ class MongoTypeSwitchSpec extends AnyWordSpec with Matchers { case class B(int: Int) extends A case class C(int: Int) extends A @MongoTypeHint("D2") case class D(int: Int) extends A - - - "deriving TypedMongoFormat" must { + + "mongoTypeSwitch" must { "derive a subset of a sealed trait" in { val format = generic.mongoTypeSwitch[A, (B, C)]() @@ -45,6 +44,5 @@ class MongoTypeSwitchSpec extends AnyWordSpec with Matchers { d2 must be(d) } - } } From 868050b68bd65c08b4cce59d979e5d8f4f0eb746 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 21 Feb 2025 11:20:54 +0100 Subject: [PATCH 054/142] merge compiler option switches --- build.sbt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/build.sbt b/build.sbt index ef2aecea..b2014799 100644 --- a/build.sbt +++ b/build.sbt @@ -61,14 +61,12 @@ lazy val standardSettings = Defaults.coreDefaultSettings ++ Seq( "-deprecation", "-unchecked", "-feature" - ) ++ (CrossVersion.partialVersion(scalaVersion.value) match { - case Some((3, _)) => Seq("-noindent") - case _ => Seq.empty - }), + ), javacOptions ++= Seq("-deprecation", "-Xlint:unchecked"), // targets Java 8 bytecode (scalac & javac) scalacOptions ++= { - if (scalaVersion.value.startsWith("2.12") || scalaVersion.value.startsWith("3")) Seq.empty + if (scalaVersion.value.startsWith("2.12")) Seq.empty + else if (scalaVersion.value.startsWith("3")) Seq("-noindent") else Seq("-target", "8") }, ThisBuild / javacOptions ++= Seq("-source", "8", "-target", "8"), From c6bf6205306d41b19e7cbb84a34aab602517f880 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 21 Feb 2025 11:47:14 +0100 Subject: [PATCH 055/142] Remove util-3 as util is compatible with both scala 2 and 3 --- build.sbt | 18 +- json/json-3/dependencies.sbt | 4 +- util-3/dependencies.sbt | 7 - util-3/src/main/scala/Concurrent.scala | 13 - util-3/src/main/scala/LangTag.scala | 19 - util-3/src/main/scala/Logging.scala | 5 - util-3/src/main/scala/Memoizer.scala | 28 - util-3/src/main/scala/Money.scala | 666 ------------------ util-3/src/main/scala/Reflect.scala | 61 -- util-3/src/main/scala/ValidatedFlatMap.scala | 23 - util-3/src/test/scala/DomainObjectsGen.scala | 25 - .../test/scala/HighPrecisionMoneySpec.scala | 218 ------ util-3/src/test/scala/LangTagSpec.scala | 27 - util-3/src/test/scala/MoneySpec.scala | 162 ----- .../scala/ScalaLoggingCompatiblitySpec.scala | 18 - util/dependencies.sbt | 2 +- 16 files changed, 12 insertions(+), 1284 deletions(-) delete mode 100644 util-3/dependencies.sbt delete mode 100644 util-3/src/main/scala/Concurrent.scala delete mode 100644 util-3/src/main/scala/LangTag.scala delete mode 100644 util-3/src/main/scala/Logging.scala delete mode 100644 util-3/src/main/scala/Memoizer.scala delete mode 100644 util-3/src/main/scala/Money.scala delete mode 100644 util-3/src/main/scala/Reflect.scala delete mode 100644 util-3/src/main/scala/ValidatedFlatMap.scala delete mode 100644 util-3/src/test/scala/DomainObjectsGen.scala delete mode 100644 util-3/src/test/scala/HighPrecisionMoneySpec.scala delete mode 100644 util-3/src/test/scala/LangTagSpec.scala delete mode 100644 util-3/src/test/scala/MoneySpec.scala delete mode 100644 util-3/src/test/scala/ScalaLoggingCompatiblitySpec.scala diff --git a/build.sbt b/build.sbt index b2014799..d110f21d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,7 +23,13 @@ ThisBuild / githubWorkflowBuild := Seq( name = Some("Build Scala 2 project"), cond = Some(s"matrix.scala != '$scala3'")), WorkflowStep.Sbt( - commands = List("sphere-util/test", "sphere-json-core/test", "sphere-mongo-core/test"), + commands = List( + "sphere-util/test", + "sphere-json-core/test", + "sphere-mongo-core/test", + "sphere-mongo-3/test", + "sphere-json-3/test" + ), name = Some("Build Scala 3 project"), cond = Some(s"matrix.scala == '$scala3'") ) @@ -88,8 +94,8 @@ lazy val `sphere-libs` = project .settings(publishArtifact := false, publish := {}, crossScalaVersions := Seq()) .aggregate( // Scala 3 modules - `sphere-util-3`, `sphere-json-3`, + `sphere-mongo-3`, // Scala 2 modules `sphere-util`, @@ -105,17 +111,11 @@ lazy val `sphere-libs` = project // Scala 3 modules -lazy val `sphere-util-3` = project - .in(file("./util-3")) - .settings(scalaVersion := scala3) - .settings(standardSettings: _*) - .settings(homepage := Some(url("https://github.com/commercetools/sphere-scala-libs/README.md"))) - lazy val `sphere-json-3` = project .in(file("./json/json-3")) .settings(scalaVersion := scala3) .settings(standardSettings: _*) - .dependsOn(`sphere-util-3`) + .dependsOn(`sphere-util`) // Scala 2 modules diff --git a/json/json-3/dependencies.sbt b/json/json-3/dependencies.sbt index 0dc28bc8..22bf4db4 100644 --- a/json/json-3/dependencies.sbt +++ b/json/json-3/dependencies.sbt @@ -1,5 +1,5 @@ libraryDependencies ++= Seq( - "org.json4s" %% "json4s-jackson" % "4.0.7", + ("org.json4s" %% "json4s-jackson" % "4.0.7").cross(CrossVersion.for3Use2_13), "com.fasterxml.jackson.core" % "jackson-databind" % "2.17.2", - "org.typelevel" %% "cats-core" % "2.12.0" + ("org.typelevel" %% "cats-core" % "2.13.0").cross(CrossVersion.for3Use2_13) ) diff --git a/util-3/dependencies.sbt b/util-3/dependencies.sbt deleted file mode 100644 index cb4ce29b..00000000 --- a/util-3/dependencies.sbt +++ /dev/null @@ -1,7 +0,0 @@ -libraryDependencies ++= Seq( - "com.typesafe.scala-logging" %% "scala-logging" % "3.9.4", - "joda-time" % "joda-time" % "2.12.7", - "org.joda" % "joda-convert" % "2.2.3", - ("org.typelevel" % "cats-core" % "2.12.0").cross(CrossVersion.binary), - "org.json4s" %% "json4s-scalap" % "4.0.7" -) diff --git a/util-3/src/main/scala/Concurrent.scala b/util-3/src/main/scala/Concurrent.scala deleted file mode 100644 index 4dcbf7ca..00000000 --- a/util-3/src/main/scala/Concurrent.scala +++ /dev/null @@ -1,13 +0,0 @@ -package io.sphere.util - -import java.util.concurrent.ThreadFactory -import java.util.concurrent.atomic.AtomicInteger - -object Concurrent { - def namedThreadFactory(poolName: String): ThreadFactory = - new ThreadFactory { - val count = new AtomicInteger(0) - override def newThread(r: Runnable) = - new Thread(r, poolName + "-" + count.incrementAndGet) - } -} diff --git a/util-3/src/main/scala/LangTag.scala b/util-3/src/main/scala/LangTag.scala deleted file mode 100644 index 0005c9f3..00000000 --- a/util-3/src/main/scala/LangTag.scala +++ /dev/null @@ -1,19 +0,0 @@ -package io.sphere.util - -import java.util.Locale - -/** Extractor for Locales, e.g. for use in pattern-matching request paths. */ -object LangTag { - - final val UNDEFINED: String = "und" - - class LocaleOpt(val locale: Locale) extends AnyVal { - // if toLanguageTag returns "und", it means the language tag is undefined - def isEmpty: Boolean = UNDEFINED == locale.toLanguageTag - def get: Locale = locale - } - - def unapply(s: String): LocaleOpt = new LocaleOpt(Locale.forLanguageTag(s)) - - def invalidLangTagMessage(invalidLangTag: String) = s"Invalid language tag: '$invalidLangTag'" -} diff --git a/util-3/src/main/scala/Logging.scala b/util-3/src/main/scala/Logging.scala deleted file mode 100644 index e5035c4e..00000000 --- a/util-3/src/main/scala/Logging.scala +++ /dev/null @@ -1,5 +0,0 @@ -package io.sphere.util - -trait Logging extends com.typesafe.scalalogging.StrictLogging { - protected val log = logger -} diff --git a/util-3/src/main/scala/Memoizer.scala b/util-3/src/main/scala/Memoizer.scala deleted file mode 100644 index 9c6ea674..00000000 --- a/util-3/src/main/scala/Memoizer.scala +++ /dev/null @@ -1,28 +0,0 @@ -package io.sphere.util - -import java.util.concurrent._ - -/** Straight port from the Java impl. of "Java Concurrency in Practice". */ -final class Memoizer[K, V](action: K => V) extends (K => V) { - private val cache = new ConcurrentHashMap[K, Future[V]] - def apply(k: K): V = { - while (true) { - var f = cache.get(k) - if (f == null) { - val eval = new Callable[V] { def call(): V = action(k) } - val ft = new FutureTask[V](eval) - f = cache.putIfAbsent(k, ft) - if (f == null) { - f = ft - ft.run() - } - } - try return f.get - catch { - case _: CancellationException => cache.remove(k, f) - case e: ExecutionException => throw e.getCause - } - } - sys.error("Failed to compute result.") - } -} diff --git a/util-3/src/main/scala/Money.scala b/util-3/src/main/scala/Money.scala deleted file mode 100644 index b3ffdac7..00000000 --- a/util-3/src/main/scala/Money.scala +++ /dev/null @@ -1,666 +0,0 @@ -package io.sphere.util - -import language.implicitConversions -import java.math.MathContext -import java.text.NumberFormat -import java.util.{Currency, Locale} - -import cats.Monoid -import cats.data.ValidatedNel -import cats.syntax.validated._ - -import scala.math._ -import BigDecimal.RoundingMode._ -import scala.math.BigDecimal.RoundingMode -import ValidatedFlatMapFeature._ -import io.sphere.util.BaseMoney.bigDecimalToMoneyLong -import io.sphere.util.Money.ImplicitsDecimal.MoneyNotation - -class MoneyOverflowException extends RuntimeException("A Money operation resulted in an overflow.") - -sealed trait BaseMoney { - def `type`: String - - def currency: Currency - - // Use with CAUTION! will loose precision in case of a high precision money value - def centAmount: Long - - /** Normalized representation. - * - * for centPrecision: - * - centAmount: 1234 EUR - * - amount: 12.34 - * - * for highPrecision: preciseAmount: - * - 123456 EUR (with fractionDigits = 4) - * - amount: 12.3456 - */ - def amount: BigDecimal - - def fractionDigits: Int - - def toMoneyWithPrecisionLoss: Money - - def +(m: Money)(implicit mode: RoundingMode): BaseMoney - def +(m: HighPrecisionMoney)(implicit mode: RoundingMode): BaseMoney - def +(m: BaseMoney)(implicit mode: RoundingMode): BaseMoney - def +(m: BigDecimal)(implicit mode: RoundingMode): BaseMoney - - def -(m: Money)(implicit mode: RoundingMode): BaseMoney - def -(m: HighPrecisionMoney)(implicit mode: RoundingMode): BaseMoney - def -(m: BaseMoney)(implicit mode: RoundingMode): BaseMoney - def -(m: BigDecimal)(implicit mode: RoundingMode): BaseMoney - - def *(m: Money)(implicit mode: RoundingMode): BaseMoney - def *(m: HighPrecisionMoney)(implicit mode: RoundingMode): BaseMoney - def *(m: BaseMoney)(implicit mode: RoundingMode): BaseMoney - def *(m: BigDecimal)(implicit mode: RoundingMode): BaseMoney -} - -object BaseMoney { - val TypeField: String = "type" - - def requireSameCurrency(m1: BaseMoney, m2: BaseMoney): Unit = - require(m1.currency eq m2.currency, s"${m1.currency} != ${m2.currency}") - - def toScalaRoundingMode(mode: java.math.RoundingMode): RoundingMode.Value = - BigDecimal.RoundingMode(mode.ordinal) - - implicit def baseMoneyMonoid(implicit c: Currency, mode: RoundingMode): Monoid[BaseMoney] = - new Monoid[BaseMoney] { - def combine(x: BaseMoney, y: BaseMoney): BaseMoney = x + y - val empty: BaseMoney = Money.zero(c) - } - - private[util] def bigDecimalToMoneyLong(amount: BigDecimal): Long = - try amount.toLongExact - catch { case _: ArithmeticException => throw new MoneyOverflowException } -} - -/** Represents an amount of money in a certain currency. - * - * This implementation does not support fractional money units (eg a tenth cent). Amounts are - * always rounded to the nearest, smallest unit of the respective currency. The rounding mode can - * be specified using an implicit `BigDecimal.RoundingMode`. - * - * @param centAmount - * The amount in the smallest indivisible unit of the respective currency represented as a single - * Long value. - * @param currency - * The currency of the amount. - */ -case class Money private[util] (centAmount: Long, currency: Currency) - extends BaseMoney - with Ordered[Money] { - import Money._ - - private val centFactor: Double = 1 / pow(10, currency.getDefaultFractionDigits) - private val backwardsCompatibleRoundingModeForOperations = BigDecimal.RoundingMode.HALF_EVEN - - val `type`: String = TypeName - - override def fractionDigits: Int = currency.getDefaultFractionDigits - override lazy val amount: BigDecimal = BigDecimal(centAmount) * cachedCentFactor(fractionDigits) - - def withCentAmount(centAmount: Long): Money = - copy(centAmount = centAmount) - - def toHighPrecisionMoney(fractionDigits: Int): HighPrecisionMoney = - HighPrecisionMoney.fromMoney(this, fractionDigits) - - /** Creates a new Money instance with the same currency and the amount conforming to the given - * MathContext (scale and rounding mode). - */ - def apply(mc: MathContext): Money = - fromDecimalAmount(this.amount(mc), this.currency)(RoundingMode.HALF_EVEN) - - def +(m: Money)(implicit mode: RoundingMode): Money = { - BaseMoney.requireSameCurrency(this, m) - - fromDecimalAmount(this.amount + m.amount, this.currency)( - backwardsCompatibleRoundingModeForOperations) - } - - def +(m: HighPrecisionMoney)(implicit mode: RoundingMode): HighPrecisionMoney = - this.toHighPrecisionMoney(m.fractionDigits) + m - - def +(money: BaseMoney)(implicit mode: RoundingMode): BaseMoney = money match { - case m: Money => this + m - case m: HighPrecisionMoney => this + m - } - - def +(m: BigDecimal)(implicit mode: RoundingMode): Money = - this + fromDecimalAmount(m, this.currency) - - def -(m: Money)(implicit mode: RoundingMode): Money = { - BaseMoney.requireSameCurrency(this, m) - fromDecimalAmount(this.amount - m.amount, this.currency) - } - - def -(money: BaseMoney)(implicit mode: RoundingMode): BaseMoney = money match { - case m: Money => this - m - case m: HighPrecisionMoney => this - m - } - - def -(m: HighPrecisionMoney)(implicit mode: RoundingMode): HighPrecisionMoney = - this.toHighPrecisionMoney(m.fractionDigits) - m - - def -(m: BigDecimal)(implicit mode: RoundingMode): Money = - this - fromDecimalAmount(m, this.currency) - - def *(m: Money)(implicit mode: RoundingMode): Money = { - BaseMoney.requireSameCurrency(this, m) - this * m.amount - } - - def *(m: HighPrecisionMoney)(implicit mode: RoundingMode): HighPrecisionMoney = - this.toHighPrecisionMoney(m.fractionDigits) * m - - def *(money: BaseMoney)(implicit mode: RoundingMode): BaseMoney = money match { - case m: Money => this * m - case m: HighPrecisionMoney => this * m - } - - def *(m: BigDecimal)(implicit mode: RoundingMode): Money = - fromDecimalAmount((this.amount * m).setScale(this.amount.scale, mode), this.currency) - - /** Divide to integral value + remainder */ - def /%(m: BigDecimal)(implicit mode: RoundingMode): (Money, Money) = { - val (result, remainder) = this.amount /% m - - (fromDecimalAmount(result, this.currency), fromDecimalAmount(remainder, this.currency)) - } - - def %(m: Money)(implicit mode: RoundingMode): Money = this.remainder(m) - - def %(m: BigDecimal)(implicit mode: RoundingMode): Money = - this.remainder(fromDecimalAmount(m, this.currency)) - - def remainder(m: Money)(implicit mode: RoundingMode): Money = { - BaseMoney.requireSameCurrency(this, m) - - fromDecimalAmount(this.amount.remainder(m.amount), this.currency) - } - - def remainder(m: BigDecimal)(implicit mode: RoundingMode): Money = - this.remainder(fromDecimalAmount(m, this.currency)(RoundingMode.HALF_EVEN)) - - def unary_- : Money = - fromDecimalAmount(-this.amount, this.currency)(BigDecimal.RoundingMode.UNNECESSARY) - - /** Partitions this amount of money into several parts where the size of the individual parts are - * defined by the given ratios. The partitioning takes care of not losing or gaining any money by - * distributing any remaining "cents" evenly across the partitions. - * - *

Example: (0.05 EUR) partition (3,7) == Seq(0.02 EUR, 0.03 EUR)

- */ - def partition(ratios: Int*): Seq[Money] = { - val total = ratios.sum - val amountInCents = BigInt(this.centAmount) - val amounts = ratios.map(amountInCents * _ / total) - var remainder = amounts.foldLeft(amountInCents)(_ - _) - amounts.map { amount => - remainder -= 1 - fromDecimalAmount( - BigDecimal(amount + (if (remainder >= 0) 1 else 0)) * centFactor, - this.currency)(backwardsCompatibleRoundingModeForOperations) - } - } - - def toMoneyWithPrecisionLoss: Money = this - - def compare(that: Money): Int = { - BaseMoney.requireSameCurrency(this, that) - this.centAmount.compare(that.centAmount) - } - - override def toString: String = Money.toString(centAmount, fractionDigits, currency) - - def toString(nf: NumberFormat, locale: Locale): String = { - require(nf.getCurrency eq this.currency) - nf.format(this.amount.doubleValue) + " " + this.currency.getSymbol(locale) - } -} - -object Money { - object ImplicitsDecimal { - final implicit class MoneyNotation(val amount: BigDecimal) extends AnyVal { - def EUR: Money = Money.EUR(amount) - def USD: Money = Money.USD(amount) - def GBP: Money = Money.GBP(amount) - def JPY: Money = Money.JPY(amount) - } - - implicit def doubleMoneyNotation(amount: Double): MoneyNotation = - new ImplicitsDecimal.MoneyNotation(BigDecimal(amount)) - } - - object ImplicitsString { - implicit def stringMoneyNotation(amount: String): MoneyNotation = - new ImplicitsDecimal.MoneyNotation(BigDecimal(amount)) - } - - private def decimalAmountWithCurrencyAndHalfEvenRounding(amount: BigDecimal, currency: String) = - fromDecimalAmount(amount, Currency.getInstance(currency))(BigDecimal.RoundingMode.HALF_EVEN) - - def EUR(amount: BigDecimal): Money = decimalAmountWithCurrencyAndHalfEvenRounding(amount, "EUR") - def USD(amount: BigDecimal): Money = decimalAmountWithCurrencyAndHalfEvenRounding(amount, "USD") - def GBP(amount: BigDecimal): Money = decimalAmountWithCurrencyAndHalfEvenRounding(amount, "GBP") - def JPY(amount: BigDecimal): Money = decimalAmountWithCurrencyAndHalfEvenRounding(amount, "JPY") - - final val CurrencyCodeField: String = "currencyCode" - final val CentAmountField: String = "centAmount" - final val FractionDigitsField: String = "fractionDigits" - final val TypeName: String = "centPrecision" - - def fromDecimalAmount(amount: BigDecimal, currency: Currency)(implicit - mode: RoundingMode): Money = { - val fractionDigits = currency.getDefaultFractionDigits - val centAmountBigDecimal = amount * cachedCentPower(fractionDigits) - val centAmountBigDecimalZeroScale = centAmountBigDecimal.setScale(0, mode) - Money(bigDecimalToMoneyLong(centAmountBigDecimalZeroScale), currency) - } - - def apply(amount: BigDecimal, currency: Currency): Money = { - println("this is called") - require( - amount.scale == currency.getDefaultFractionDigits, - "The scale of the given amount does not match the scale of the provided currency." + - " - " + amount.scale + " <-> " + currency.getDefaultFractionDigits - ) - fromDecimalAmount(amount, currency)(BigDecimal.RoundingMode.UNNECESSARY) - } - - private final val bdOne: BigDecimal = BigDecimal(1) - final val bdTen: BigDecimal = BigDecimal(10) - - private final val centPowerZeroFractionDigit = bdOne - private final val centPowerOneFractionDigit = bdTen - private final val centPowerTwoFractionDigit = bdTen.pow(2) - private final val centPowerThreeFractionDigit = bdTen.pow(3) - private final val centPowerFourFractionDigit = bdTen.pow(4) - - private[util] def cachedCentPower(currencyFractionDigits: Int): BigDecimal = - currencyFractionDigits match { - case 0 => centPowerZeroFractionDigit - case 1 => centPowerOneFractionDigit - case 2 => centPowerTwoFractionDigit - case 3 => centPowerThreeFractionDigit - case 4 => centPowerFourFractionDigit - case other => bdTen.pow(other) - } - - private val centFactorZeroFractionDigit = bdOne / bdTen.pow(0) - private val centFactorOneFractionDigit = bdOne / bdTen.pow(1) - private val centFactorTwoFractionDigit = bdOne / bdTen.pow(2) - private val centFactorThreeFractionDigit = bdOne / bdTen.pow(3) - private val centFactorFourFractionDigit = bdOne / bdTen.pow(4) - - private[util] def cachedCentFactor(currencyFractionDigits: Int): BigDecimal = - currencyFractionDigits match { - case 0 => centFactorZeroFractionDigit - case 1 => centFactorOneFractionDigit - case 2 => centFactorTwoFractionDigit - case 3 => centFactorThreeFractionDigit - case 4 => centFactorFourFractionDigit - case other => bdOne / bdTen.pow(other) - } - - def fromCentAmount(centAmount: Long, currency: Currency): Money = - new Money(centAmount, currency) - - private val cachedZeroEUR = fromCentAmount(0L, Currency.getInstance("EUR")) - private val cachedZeroUSD = fromCentAmount(0L, Currency.getInstance("USD")) - private val cachedZeroGBP = fromCentAmount(0L, Currency.getInstance("GBP")) - private val cachedZeroJPY = fromCentAmount(0L, Currency.getInstance("JPY")) - - def zero(currency: Currency): Money = - currency.getCurrencyCode match { - case "EUR" => cachedZeroEUR - case "USD" => cachedZeroUSD - case "GBP" => cachedZeroGBP - case "JPY" => cachedZeroJPY - case _ => fromCentAmount(0L, currency) - } - - implicit def moneyMonoid(implicit c: Currency, mode: RoundingMode): Monoid[Money] = - new Monoid[Money] { - def combine(x: Money, y: Money): Money = x + y - val empty: Money = Money.zero(c) - } - - def toString(amount: Long, fractionDigits: Int, currency: Currency): String = { - val amountDigits = amount.toString.toList - val leadingZerosLength = fractionDigits - amountDigits.length + 1 - val leadingZeros = List.fill(leadingZerosLength)('0') - val allDigits = leadingZeros ::: amountDigits - val radixPosition = allDigits.length - fractionDigits - val (integer, fractional) = allDigits.splitAt(radixPosition) - if (fractional.nonEmpty) - s"${integer.mkString}.${fractional.mkString} ${currency.getCurrencyCode}" - else - s"${integer.mkString} ${currency.getCurrencyCode}" - } -} - -case class HighPrecisionMoney private ( - preciseAmount: Long, - fractionDigits: Int, - centAmount: Long, - currency: Currency) - extends BaseMoney - with Ordered[Money] { - import HighPrecisionMoney._ - - require( - fractionDigits >= currency.getDefaultFractionDigits, - "`fractionDigits` should be >= than the default fraction digits of the currency.") - - val `type`: String = TypeName - - lazy val amount: BigDecimal = - (BigDecimal(preciseAmount) * factor(fractionDigits)).setScale(fractionDigits) - - def withFractionDigits(fd: Int)(implicit mode: RoundingMode): HighPrecisionMoney = { - val scaledAmount = amount.setScale(fd, mode) - val newCentAmount = roundToCents(scaledAmount, currency) - HighPrecisionMoney(amountToPreciseAmount(scaledAmount, fd), fd, newCentAmount, currency) - } - - def updateCentAmountWithRoundingMode(implicit mode: RoundingMode): HighPrecisionMoney = - copy(centAmount = roundToCents(amount, currency)) - - def +(other: HighPrecisionMoney)(implicit mode: RoundingMode): HighPrecisionMoney = - calc(this, other, _ + _) - - def +(m: Money)(implicit mode: RoundingMode): HighPrecisionMoney = - this + m.toHighPrecisionMoney(fractionDigits) - - def +(money: BaseMoney)(implicit mode: RoundingMode): HighPrecisionMoney = money match { - case m: Money => this + m - case m: HighPrecisionMoney => this + m - } - - def +(other: BigDecimal)(implicit mode: RoundingMode): HighPrecisionMoney = - this + fromDecimalAmount(other, this.fractionDigits, this.currency) - - def -(other: HighPrecisionMoney)(implicit mode: RoundingMode): HighPrecisionMoney = - calc(this, other, _ - _) - - def -(m: Money)(implicit mode: RoundingMode): HighPrecisionMoney = - this - m.toHighPrecisionMoney(fractionDigits) - - def -(money: BaseMoney)(implicit mode: RoundingMode): HighPrecisionMoney = money match { - case m: Money => this - m - case m: HighPrecisionMoney => this - m - } - - def -(other: BigDecimal)(implicit mode: RoundingMode): HighPrecisionMoney = - this - fromDecimalAmount(other, this.fractionDigits, this.currency) - - def *(other: HighPrecisionMoney)(implicit mode: RoundingMode): HighPrecisionMoney = - calc(this, other, _ * _) - - def *(m: Money)(implicit mode: RoundingMode): HighPrecisionMoney = - this * m.toHighPrecisionMoney(fractionDigits) - - def *(money: BaseMoney)(implicit mode: RoundingMode): HighPrecisionMoney = money match { - case m: Money => this * m - case m: HighPrecisionMoney => this * m - } - - def *(other: BigDecimal)(implicit mode: RoundingMode): HighPrecisionMoney = - this * fromDecimalAmount(other, this.fractionDigits, this.currency) - - /** Divide to integral value + remainder */ - def /%(m: BigDecimal)(implicit mode: RoundingMode): (HighPrecisionMoney, HighPrecisionMoney) = { - val (result, remainder) = this.amount /% m - - fromDecimalAmount(result, fractionDigits, this.currency) -> - fromDecimalAmount(remainder, fractionDigits, this.currency) - } - - def %(other: HighPrecisionMoney)(implicit mode: RoundingMode): HighPrecisionMoney = - this.remainder(other) - - def %(m: Money)(implicit mode: RoundingMode): HighPrecisionMoney = - this.remainder(m.toHighPrecisionMoney(fractionDigits)) - - def %(other: BigDecimal)(implicit mode: RoundingMode): HighPrecisionMoney = - this.remainder(fromDecimalAmount(other, this.fractionDigits, this.currency)) - - def remainder(other: HighPrecisionMoney)(implicit mode: RoundingMode): HighPrecisionMoney = - calc(this, other, _ remainder _) - - def remainder(other: BigDecimal)(implicit mode: RoundingMode): HighPrecisionMoney = - this.remainder(fromDecimalAmount(other, this.fractionDigits, this.currency)) - - def unary_- : HighPrecisionMoney = - fromDecimalAmount(-this.amount, this.fractionDigits, this.currency)( - BigDecimal.RoundingMode.UNNECESSARY) - - /** Partitions this amount of money into several parts where the size of the individual parts are - * defined by the given ratios. The partitioning takes care of not losing or gaining any money by - * distributing any remaining "cents" evenly across the partitions. - * - *

Example: (0.05 EUR) partition (3,7) == Seq(0.02 EUR, 0.03 EUR)

- */ - def partition(ratios: Int*)(implicit mode: RoundingMode): Seq[HighPrecisionMoney] = { - val total = ratios.sum - val factor = Money.cachedCentFactor(fractionDigits) - val amountAsInt = BigInt(this.preciseAmount) - val portionAmounts = ratios.map(amountAsInt * _ / total) - var remainder = portionAmounts.foldLeft(amountAsInt)(_ - _) - - portionAmounts.map { portionAmount => - remainder -= 1 - - fromDecimalAmount( - BigDecimal(portionAmount + (if (remainder >= 0) 1 else 0)) * factor, - this.fractionDigits, - this.currency) - } - } - - def toMoneyWithPrecisionLoss: Money = - Money.fromCentAmount(this.centAmount, currency) - - def compare(other: Money): Int = { - BaseMoney.requireSameCurrency(this, other) - - this.amount.compare(other.amount) - } - - override def toString: String = Money.toString(preciseAmount, fractionDigits, currency) - - def toString(nf: NumberFormat, locale: Locale): String = { - require(nf.getCurrency eq this.currency) - - nf.format(this.amount.doubleValue) + " " + this.currency.getSymbol(locale) - } -} - -object HighPrecisionMoney { - object ImplicitsDecimal { - final implicit class HighPrecisionMoneyNotation(val amount: BigDecimal) extends AnyVal { - def EUR: HighPrecisionMoney = HighPrecisionMoney.EUR(amount) - def USD: HighPrecisionMoney = HighPrecisionMoney.USD(amount) - def GBP: HighPrecisionMoney = HighPrecisionMoney.GBP(amount) - def JPY: HighPrecisionMoney = HighPrecisionMoney.JPY(amount) - } - } - - object ImplicitsDecimalPrecise { - final implicit class HighPrecisionPreciseMoneyNotation(val amount: BigDecimal) extends AnyVal { - def EUR_PRECISE(precision: Int): HighPrecisionMoney = - HighPrecisionMoney.EUR(amount, Some(precision)) - def USD_PRECISE(precision: Int): HighPrecisionMoney = - HighPrecisionMoney.USD(amount, Some(precision)) - def GBP_PRECISE(precision: Int): HighPrecisionMoney = - HighPrecisionMoney.GBP(amount, Some(precision)) - def JPY_PRECISE(precision: Int): HighPrecisionMoney = - HighPrecisionMoney.JPY(amount, Some(precision)) - } - } - - object ImplicitsString { - implicit def stringMoneyNotation(amount: String): ImplicitsDecimal.HighPrecisionMoneyNotation = - new ImplicitsDecimal.HighPrecisionMoneyNotation(BigDecimal(amount)) - } - - object ImplicitsStringPrecise { - implicit def stringPreciseMoneyNotation( - amount: String): ImplicitsDecimalPrecise.HighPrecisionPreciseMoneyNotation = - new ImplicitsDecimalPrecise.HighPrecisionPreciseMoneyNotation(BigDecimal(amount)) - } - - def EUR(amount: BigDecimal, fractionDigits: Option[Int] = None): HighPrecisionMoney = - simpleValueMeantToBeUsedOnlyInTests(amount, "EUR", fractionDigits) - def USD(amount: BigDecimal, fractionDigits: Option[Int] = None): HighPrecisionMoney = - simpleValueMeantToBeUsedOnlyInTests(amount, "USD", fractionDigits) - def GBP(amount: BigDecimal, fractionDigits: Option[Int] = None): HighPrecisionMoney = - simpleValueMeantToBeUsedOnlyInTests(amount, "GBP", fractionDigits) - def JPY(amount: BigDecimal, fractionDigits: Option[Int] = None): HighPrecisionMoney = - simpleValueMeantToBeUsedOnlyInTests(amount, "JPY", fractionDigits) - - val CurrencyCodeField: String = "currencyCode" - val CentAmountField: String = "centAmount" - val PreciseAmountField: String = "preciseAmount" - val FractionDigitsField: String = "fractionDigits" - - val TypeName: String = "highPrecision" - val MaxFractionDigits = 20 - - private def simpleValueMeantToBeUsedOnlyInTests( - amount: BigDecimal, - currencyCode: String, - fractionDigits: Option[Int]): HighPrecisionMoney = { - val currency = Currency.getInstance(currencyCode) - val fd = fractionDigits.getOrElse(currency.getDefaultFractionDigits) - - fromDecimalAmount(amount, fd, currency)(BigDecimal.RoundingMode.HALF_EVEN) - } - - def roundToCents(amount: BigDecimal, currency: Currency)(implicit mode: RoundingMode): Long = - bigDecimalToMoneyLong( - amount.setScale(currency.getDefaultFractionDigits, mode) / centFactor(currency)) - - def sameScale(m1: HighPrecisionMoney, m2: HighPrecisionMoney): (BigDecimal, BigDecimal, Int) = { - val newFractionDigits = math.max(m1.fractionDigits, m2.fractionDigits) - - def scale(m: HighPrecisionMoney, s: Int) = - if (m.fractionDigits < s) m.amount.setScale(s) - else if (m.fractionDigits == s) m.amount - else throw new IllegalStateException("Downscale is not allowed/expected at this point!") - - (scale(m1, newFractionDigits), scale(m2, newFractionDigits), newFractionDigits) - } - - def calc( - m1: HighPrecisionMoney, - m2: HighPrecisionMoney, - fn: (BigDecimal, BigDecimal) => BigDecimal)(implicit - mode: RoundingMode): HighPrecisionMoney = { - BaseMoney.requireSameCurrency(m1, m2) - - val (a1, a2, fd) = sameScale(m1, m2) - - fromDecimalAmount(fn(a1, a2), fd, m1.currency) - } - - def factor(fractionDigits: Int): BigDecimal = Money.cachedCentFactor(fractionDigits) - def centFactor(currency: Currency): BigDecimal = factor(currency.getDefaultFractionDigits) - - private def amountToPreciseAmount(amount: BigDecimal, fractionDigits: Int): Long = - bigDecimalToMoneyLong(amount * Money.cachedCentPower(fractionDigits)) - - def fromDecimalAmount(amount: BigDecimal, fractionDigits: Int, currency: Currency)(implicit - mode: RoundingMode): HighPrecisionMoney = { - val scaledAmount = amount.setScale(fractionDigits, mode) - val preciseAmount = amountToPreciseAmount(scaledAmount, fractionDigits) - val newCentAmount = roundToCents(scaledAmount, currency) - HighPrecisionMoney(preciseAmount, fractionDigits, newCentAmount, currency) - } - - private def centToPreciseAmount( - centAmount: Long, - fractionDigits: Int, - currency: Currency): Long = { - val centDigits = fractionDigits - currency.getDefaultFractionDigits - if (centDigits >= 19) - throw new IllegalArgumentException("Cannot represent number bigger than 10^19 with a Long") - else - Math.pow(10, centDigits).toLong * centAmount - } - - def fromCentAmount( - centAmount: Long, - fractionDigits: Int, - currency: Currency): HighPrecisionMoney = - HighPrecisionMoney( - centToPreciseAmount(centAmount, fractionDigits, currency), - fractionDigits, - centAmount, - currency) - - def zero(fractionDigits: Int, currency: Currency): HighPrecisionMoney = - fromCentAmount(0L, fractionDigits, currency) - - /* centAmount provides an escape hatch in cases where the default rounding mode is not applicable */ - def fromPreciseAmount( - preciseAmount: Long, - fractionDigits: Int, - currency: Currency, - centAmount: Option[Long]): ValidatedNel[String, HighPrecisionMoney] = - for { - fd <- validateFractionDigits(fractionDigits, currency) - amount = BigDecimal(preciseAmount) * factor(fd) - scaledAmount = amount.setScale(fd, BigDecimal.RoundingMode.UNNECESSARY) - ca <- validateCentAmount(scaledAmount, centAmount, currency) - // TODO: revisit this part! the rounding mode might be dynamic and configured elsewhere - actualCentAmount = ca.getOrElse( - roundToCents(scaledAmount, currency)(BigDecimal.RoundingMode.HALF_EVEN)) - } yield HighPrecisionMoney(preciseAmount, fd, actualCentAmount, currency) - - private def validateFractionDigits( - fractionDigits: Int, - currency: Currency): ValidatedNel[String, Int] = - if (fractionDigits <= currency.getDefaultFractionDigits) - s"fractionDigits must be > ${currency.getDefaultFractionDigits} (default fraction digits defined by currency ${currency.getCurrencyCode}).".invalidNel - else if (fractionDigits > MaxFractionDigits) - s"fractionDigits must be <= $MaxFractionDigits.".invalidNel - else - fractionDigits.validNel - - private def validateCentAmount( - amount: BigDecimal, - centAmount: Option[Long], - currency: Currency): ValidatedNel[String, Option[Long]] = - centAmount match { - case Some(actual) => - val min = roundToCents(amount, currency)(RoundingMode.FLOOR) - val max = roundToCents(amount, currency)(RoundingMode.CEILING) - - if (actual < min || actual > max) - s"centAmount must be correctly rounded preciseAmount (a number between $min and $max).".invalidNel - else - centAmount.validNel - - case _ => - centAmount.validNel - } - - def fromMoney(money: Money, fractionDigits: Int): HighPrecisionMoney = - HighPrecisionMoney( - centToPreciseAmount(money.centAmount, fractionDigits, money.currency), - fractionDigits, - money.centAmount, - money.currency) - - def monoid(fractionDigits: Int, c: Currency)(implicit - mode: RoundingMode): Monoid[HighPrecisionMoney] = new Monoid[HighPrecisionMoney] { - def combine(x: HighPrecisionMoney, y: HighPrecisionMoney): HighPrecisionMoney = x + y - val empty: HighPrecisionMoney = HighPrecisionMoney.zero(fractionDigits, c) - } -} diff --git a/util-3/src/main/scala/Reflect.scala b/util-3/src/main/scala/Reflect.scala deleted file mode 100644 index bb299b67..00000000 --- a/util-3/src/main/scala/Reflect.scala +++ /dev/null @@ -1,61 +0,0 @@ -package io.sphere.util - -import org.json4s.scalap.scalasig._ - -object Reflect extends Logging { - case class CaseClassMeta(fields: IndexedSeq[CaseClassFieldMeta]) - case class CaseClassFieldMeta(name: String, default: Option[Any] = None) - - /** Obtains minimal meta information about a case class or object via scalap. The meta information - * contains a list of names and default values which represent the arguments of the case class - * constructor and their default values, in the order they are defined. - * - * Note: Does not work for case classes or objects nested in other classes or traits (nesting - * inside other objects is fine). Note: Only a single default value is obtained for each field. - * Thus avoid default values that are different on each invocation (e.g. new DateTime()). In - * other words, the case class constructors should be pure functions. - */ - val getCaseClassMeta = new Memoizer[Class[?], CaseClassMeta](clazz => { - logger.trace( - "Initializing reflection metadata for case class or object %s".format(clazz.getName)) - CaseClassMeta(getCaseClassFieldMeta(clazz)) - }) - - private def getCompanionClass(clazz: Class[?]): Class[?] = - Class.forName(clazz.getName + "$", true, clazz.getClassLoader) - private def getCompanionObject(companionClass: Class[?]): Object = - companionClass.getField("MODULE$").get(null) - private def getCaseClassFieldMeta(clazz: Class[?]): IndexedSeq[CaseClassFieldMeta] = - if (clazz.getName.endsWith("$")) IndexedSeq.empty[CaseClassFieldMeta] - else { - val companionClass = getCompanionClass(clazz) - val companionObject = getCompanionObject(companionClass) - - val maybeSym = clazz.getName.split("\\$") match { - case Array(_) => ScalaSigParser.parse(clazz).flatMap(_.topLevelClasses.headOption) - case Array(h, t @ _*) => - val name = t.last - val topSymbol = ScalaSigParser.parse(Class.forName(h, true, clazz.getClassLoader)) - topSymbol.flatMap(_.symbols.collectFirst { case s: ClassSymbol if s.name == name => s }) - } - - val sym = maybeSym.getOrElse { - throw new IllegalArgumentException( - "Unable to find class symbol through ScalaSigParser for class %s." - .format(clazz.getName)) - } - - sym.children.iterator - .collect { case m: MethodSymbol if m.isCaseAccessor && !m.isPrivate => m } - .zipWithIndex - .map { case (ms, idx) => - val defaultValue = - try Some(companionClass.getMethod("apply$default$" + (idx + 1)).invoke(companionObject)) - catch { - case _: NoSuchMethodException => None - } - CaseClassFieldMeta(ms.name, defaultValue) - } - .toIndexedSeq - } -} diff --git a/util-3/src/main/scala/ValidatedFlatMap.scala b/util-3/src/main/scala/ValidatedFlatMap.scala deleted file mode 100644 index 9c9f1991..00000000 --- a/util-3/src/main/scala/ValidatedFlatMap.scala +++ /dev/null @@ -1,23 +0,0 @@ -package io.sphere.util - -import cats.data.Validated - -class ValidatedFlatMap[E, A](val v: Validated[E, A]) extends AnyVal { - def flatMap[EE >: E, B](f: A => Validated[EE, B]): Validated[EE, B] = - v.andThen(f) -} - -/** Cats [[Validated]] does not provide `flatMap` because its purpose is to accumulate errors. - * - * To combine [[Validated]] in for-comprehension, it is possible to import this implicit conversion - * - with the knowledge that the `flatMap` short-circuits errors. - * http://typelevel.org/cats/datatypes/validated.html - */ -object ValidatedFlatMapFeature { - import scala.language.implicitConversions - - @inline implicit def ValidationFlatMapRequested[E, A]( - d: Validated[E, A]): ValidatedFlatMap[E, A] = - new ValidatedFlatMap(d) - -} diff --git a/util-3/src/test/scala/DomainObjectsGen.scala b/util-3/src/test/scala/DomainObjectsGen.scala deleted file mode 100644 index b654f020..00000000 --- a/util-3/src/test/scala/DomainObjectsGen.scala +++ /dev/null @@ -1,25 +0,0 @@ -package io.sphere.util - -import java.util.Currency - -import org.scalacheck.Gen - -import scala.jdk.CollectionConverters._ - -object DomainObjectsGen { - - private val currency: Gen[Currency] = - Gen.oneOf(Currency.getAvailableCurrencies.asScala.toSeq) - - val money: Gen[Money] = for { - currency <- currency - amount <- Gen.chooseNum[Long](Long.MinValue, Long.MaxValue) - } yield Money(amount, currency) - - val highPrecisionMoney: Gen[HighPrecisionMoney] = for { - money <- money - } yield HighPrecisionMoney.fromMoney(money, money.currency.getDefaultFractionDigits) - - val baseMoney: Gen[BaseMoney] = Gen.oneOf(money, highPrecisionMoney) - -} diff --git a/util-3/src/test/scala/HighPrecisionMoneySpec.scala b/util-3/src/test/scala/HighPrecisionMoneySpec.scala deleted file mode 100644 index f73d181c..00000000 --- a/util-3/src/test/scala/HighPrecisionMoneySpec.scala +++ /dev/null @@ -1,218 +0,0 @@ -package io.sphere.util - -import java.util.Currency -import cats.data.Validated.Invalid -import io.sphere.util.HighPrecisionMoney.ImplicitsDecimalPrecise.HighPrecisionPreciseMoneyNotation -import org.scalatest.funspec.AnyFunSpec -import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks -import org.scalatest.matchers.must.Matchers - -import scala.collection.mutable.ArrayBuffer -import scala.language.postfixOps -import scala.math.BigDecimal - -class HighPrecisionMoneySpec extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks { - import HighPrecisionMoney.ImplicitsString._ - import HighPrecisionMoney.ImplicitsStringPrecise._ - - implicit val defaultRoundingMode: BigDecimal.RoundingMode.Value = - BigDecimal.RoundingMode.HALF_EVEN - - val Euro: Currency = Currency.getInstance("EUR") - - describe("High Precision Money") { - - it("should allow creation of high precision money") { - ("0.01".EUR) must equal("0.01".EUR) - } - - it( - "should not allow creation of high precision money with less fraction digits than the currency has") { - val thrown = intercept[IllegalArgumentException] { - "0.01".EUR_PRECISE(1) - } - - assert( - thrown.getMessage == "requirement failed: `fractionDigits` should be >= than the default fraction digits of the currency.") - } - - it("should convert precise amount to long value correctly") { - "0.0001".EUR_PRECISE(4).preciseAmount must equal(1) - } - - it("should reduce fraction digits as expected") { - "0.0001".EUR_PRECISE(4).withFractionDigits(2).preciseAmount must equal(0) - } - - it("should support the unary '-' operator.") { - -"0.01".EUR_PRECISE(2) must equal("-0.01".EUR_PRECISE(2)) - } - - it("should throw error on overflow in the unary '-' operator.") { - a[MoneyOverflowException] must be thrownBy { - -(BigDecimal(Long.MinValue) / 1000).EUR_PRECISE(3) - } - } - - it("should support the binary '+' operator.") { - ("0.001".EUR_PRECISE(3)) + ("0.002".EUR_PRECISE(3)) must equal( - "0.003".EUR_PRECISE(3) - ) - - ("0.005".EUR_PRECISE(3)) + Money.fromDecimalAmount(BigDecimal("0.01"), Euro) must equal( - "0.015".EUR_PRECISE(3) - ) - - ("0.005".EUR_PRECISE(3)) + BigDecimal("0.005") must equal( - "0.010".EUR_PRECISE(3) - ) - } - - it("should throw error on overflow in the binary '+' operator.") { - a[MoneyOverflowException] must be thrownBy { - (BigDecimal(Long.MaxValue) / 1000).EUR_PRECISE(3) + 1 - } - } - - it("should support the binary '-' operator.") { - ("0.002".EUR_PRECISE(3)) - ("0.001".EUR_PRECISE(3)) must equal( - "0.001".EUR_PRECISE(3) - ) - - ("0.015".EUR_PRECISE(3)) - Money.fromDecimalAmount(BigDecimal("0.01"), Euro) must equal( - "0.005".EUR_PRECISE(3) - ) - - ("0.005".EUR_PRECISE(3)) - BigDecimal("0.005") must equal( - "0.000".EUR_PRECISE(3) - ) - } - - it("should throw error on overflow in the binary '-' operator.") { - a[MoneyOverflowException] must be thrownBy { - (BigDecimal(Long.MinValue) / 1000).EUR_PRECISE(3) - 1 - } - } - - it("should support the binary '*' operator.") { - ("0.002".EUR_PRECISE(3)) * ("5.00".EUR_PRECISE(2)) must equal( - "0.010".EUR_PRECISE(3) - ) - - ("0.015".EUR_PRECISE(3)) * Money.fromDecimalAmount(BigDecimal("100.00"), Euro) must equal( - "1.500".EUR_PRECISE(3) - ) - - ("0.005".EUR_PRECISE(3)) * BigDecimal("0.005") must equal( - "0.000".EUR_PRECISE(3) - ) - } - - it("should throw error on overflow in the binary '*' operator.") { - a[MoneyOverflowException] must be thrownBy { - (BigDecimal(Long.MaxValue / 1000) / 2 + 1).EUR_PRECISE(3) * 2 - } - } - - it("should support the binary '%' operator.") { - ("0.010".EUR_PRECISE(3)) % ("5.00".EUR_PRECISE(2)) must equal( - "0.010".EUR_PRECISE(3) - ) - - ("100.000".EUR_PRECISE(3)) % Money.fromDecimalAmount(BigDecimal("100.00"), Euro) must equal( - "0.000".EUR_PRECISE(3) - ) - - ("0.015".EUR_PRECISE(3)) % BigDecimal("0.002") must equal( - "0.001".EUR_PRECISE(3) - ) - } - - it("should throw error on overflow in the binary '%' operator.") { - noException must be thrownBy { - BigDecimal(Long.MaxValue / 1000).EUR_PRECISE(3) % 0.5 - } - } - - it("should support the binary '/%' operator.") { - "10.000".EUR_PRECISE(3)./%(3.00) must equal( - ("3.000".EUR_PRECISE(3), "1.000".EUR_PRECISE(3)) - ) - } - - it("should throw error on overflow in the binary '/%' operator.") { - a[MoneyOverflowException] must be thrownBy { - BigDecimal(Long.MaxValue / 1000).EUR_PRECISE(3) /% 0.5 - } - } - - it("should support the remainder operator.") { - "10.000".EUR_PRECISE(3).remainder(3.00) must equal("1.000".EUR_PRECISE(3)) - - "10.000".EUR_PRECISE(3).remainder("3.000".EUR_PRECISE(3)) must equal("1.000".EUR_PRECISE(3)) - } - - it("should not overflow when getting the remainder of a division ('%').") { - noException must be thrownBy { - BigDecimal(Long.MaxValue / 1000).EUR_PRECISE(3).remainder(0.5) - } - } - - it("should partition the value properly.") { - "10.000".EUR_PRECISE(3).partition(1, 2, 3) must equal( - ArrayBuffer( - "1.667".EUR_PRECISE(3), - "3.333".EUR_PRECISE(3), - "5.000".EUR_PRECISE(3) - ) - ) - } - - it("should validate fractionDigits (min)") { - val Invalid(errors) = HighPrecisionMoney.fromPreciseAmount(123456L, 1, Euro, None): @unchecked - - errors.toList must be( - List("fractionDigits must be > 2 (default fraction digits defined by currency EUR).")) - } - - it("should validate fractionDigits (max)") { - val Invalid(errors) = - HighPrecisionMoney.fromPreciseAmount(123456L, 100, Euro, None): @unchecked - - errors.toList must be(List("fractionDigits must be <= 20.")) - } - - it("should validate centAmount") { - val Invalid(errors) = - HighPrecisionMoney.fromPreciseAmount(123456L, 4, Euro, Some(1)): @unchecked - - errors.toList must be( - List( - "centAmount must be correctly rounded preciseAmount (a number between 1234 and 1235).")) - } - - it("should provide convenient toString") { - "10.000".EUR_PRECISE(3).toString must be("10.000 EUR") - "0.100".EUR_PRECISE(3).toString must be("0.100 EUR") - "0.010".EUR_PRECISE(3).toString must be("0.010 EUR") - "0.000".EUR_PRECISE(3).toString must be("0.000 EUR") - "94.500".EUR_PRECISE(3).toString must be("94.500 EUR") - "94".JPY_PRECISE(0).toString must be("94 JPY") - } - - it("should not fail on toString") { - forAll(DomainObjectsGen.highPrecisionMoney) { m => - m.toString - } - } - - it("should fail on too big fraction decimal") { - val thrown = intercept[IllegalArgumentException] { - val tooManyDigits = Euro.getDefaultFractionDigits + 19 - HighPrecisionMoney.fromCentAmount(100003, tooManyDigits, Euro) - } - - assert(thrown.getMessage == "Cannot represent number bigger than 10^19 with a Long") - } - } -} diff --git a/util-3/src/test/scala/LangTagSpec.scala b/util-3/src/test/scala/LangTagSpec.scala deleted file mode 100644 index 091fd6d9..00000000 --- a/util-3/src/test/scala/LangTagSpec.scala +++ /dev/null @@ -1,27 +0,0 @@ -package io.sphere.util - -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.must.Matchers - -import scala.language.postfixOps - -class LangTagSpec extends AnyFunSpec with Matchers { - describe("LangTag") { - it("should accept valid language tags") { - LangTag.unapply("de").isEmpty must be(false) - LangTag.unapply("fr").isEmpty must be(false) - LangTag.unapply("de-DE").isEmpty must be(false) - LangTag.unapply("de-AT").isEmpty must be(false) - LangTag.unapply("de-CH").isEmpty must be(false) - LangTag.unapply("fr-FR").isEmpty must be(false) - LangTag.unapply("fr-CA").isEmpty must be(false) - LangTag.unapply("he-IL-u-ca-hebrew-tz-jeruslm").isEmpty must be(false) - } - - it("should not accept invalid language tags") { - LangTag.unapply(" de").isEmpty must be(true) - LangTag.unapply("de_DE").isEmpty must be(true) - LangTag.unapply("e-DE").isEmpty must be(true) - } - } -} diff --git a/util-3/src/test/scala/MoneySpec.scala b/util-3/src/test/scala/MoneySpec.scala deleted file mode 100644 index 7d4c1a4a..00000000 --- a/util-3/src/test/scala/MoneySpec.scala +++ /dev/null @@ -1,162 +0,0 @@ -package io.sphere.util - -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.must.Matchers -import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks - -import scala.language.postfixOps -import scala.math.BigDecimal - -class MoneySpec extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks { - import Money.ImplicitsDecimal._ - import Money._ - - implicit val mode: BigDecimal.RoundingMode.Value = BigDecimal.RoundingMode.UNNECESSARY - - def euroCents(cents: Long): Money = EUR(0).withCentAmount(cents) - - describe("Money") { - it("should have value semantics.") { - (1.23 EUR) must equal(1.23 EUR) - } - - it( - "should default to HALF_EVEN rounding mode when using monetary notation and use provided rounding mode when performing operations.") { - implicit val mode = BigDecimal.RoundingMode.HALF_EVEN - - (1.001 EUR) must equal(1.00 EUR) - (1.005 EUR) must equal(1.00 EUR) - (1.015 EUR) must equal(1.02 EUR) - ((1.00 EUR) + 0.001) must equal(1.00 EUR) - ((1.00 EUR) + 0.005) must equal(1.00 EUR) - ((1.00 EUR) + 0.015) must equal(1.02 EUR) - ((1.00 EUR) - 0.005) must equal(1.00 EUR) - ((1.00 EUR) - 0.015) must equal(0.98 EUR) - ((1.00 EUR) + 0.0115) must equal(1.01 EUR) - } - - it( - "should not accept an amount with an invalid scale for the used currency when using the constructor directly.") { - an[IllegalArgumentException] must be thrownBy { - Money(1.0001, java.util.Currency.getInstance("EUR")) - } - } - - it("should not be prone to common rounding errors known from floating point numbers.") { - var m = 0.00 EUR - - for (i <- 1 to 10) m = m + 0.10 - - m must equal(1.00 EUR) - } - - it("should support the unary '-' operator.") { - -EUR(1.00) must equal(-1.00 EUR) - } - - it("should throw error on overflow in the unary '-' operator.") { - a[MoneyOverflowException] must be thrownBy { - -euroCents(Long.MinValue) - } - } - - it("should support the binary '+' operator.") { - (1.42 EUR) + (1.58 EUR) must equal(3.00 EUR) - } - - it("should support the binary '+' operator on different currencies.") { - an[IllegalArgumentException] must be thrownBy { - (1.42 EUR) + (1.58 USD) - } - } - - it("should throw error on overflow in the binary '+' operator.") { - a[MoneyOverflowException] must be thrownBy { - euroCents(Long.MaxValue) + 1 - } - } - - it("should support the binary '-' operator.") { - (1.33 EUR) - (0.33 EUR) must equal(1.00 EUR) - } - - it("should throw error on overflow in the binary '-' operator.") { - a[MoneyOverflowException] must be thrownBy { - euroCents(Long.MinValue) - 1 - } - } - - it("should support the binary '*' operator, requiring a rounding mode.") { - implicit val mode = BigDecimal.RoundingMode.HALF_EVEN - (1.33 EUR) * (1.33 EUR) must equal(1.77 EUR) - } - - it("should throw error on overflow in the binary '*' operator.") { - a[MoneyOverflowException] must be thrownBy { - euroCents(Long.MaxValue / 2 + 1) * 2 - } - } - - it("should support the binary '/%' (divideAndRemainder) operator.") { - implicit val mode = BigDecimal.RoundingMode.HALF_EVEN - (1.33 EUR) /% 0.3 must equal(4.00 EUR, 0.13 EUR) - (1.33 EUR) /% 0.003 must equal(443.00 EUR, 0.00 EUR) - } - - it("should throw error on overflow in the binary '/%' (divideAndRemainder) operator.") { - a[MoneyOverflowException] must be thrownBy { - euroCents(Long.MaxValue) /% 0.5 - } - } - - it("should support getting the remainder of a division ('%').") { - implicit val mode = BigDecimal.RoundingMode.HALF_EVEN - (1.25 EUR).remainder(1.1) must equal(0.15 EUR) - (1.25 EUR) % 1.1 must equal(0.15 EUR) - } - - it("should not overflow when getting the remainder of a division ('%').") { - noException must be thrownBy { - euroCents(Long.MaxValue).remainder(0.5) - } - } - - it("should support partitioning an amount without losing or gaining money.") { - (0.05 EUR).partition(3, 7) must equal(Seq(0.02 EUR, 0.03 EUR)) - (10 EUR).partition(1, 2) must equal(Seq(3.34 EUR, 6.66 EUR)) - (10 EUR).partition(3, 1, 3) must equal(Seq(4.29 EUR, 1.43 EUR, 4.28 EUR)) - } - - it("should allow comparing money with the same currency.") { - ((1.10 EUR) > (1.00 EUR)) must be(true) - ((1.00 EUR) >= (1.00 EUR)) must be(true) - ((1.00 EUR) < (1.10 EUR)) must be(true) - ((1.00 EUR) <= (1.00 EUR)) must be(true) - } - - it("should support currencies with a scale of 0 (i.e. Japanese Yen)") { - (1 JPY) must equal(1 JPY) - } - - it("should be able to update the centAmount") { - (1.10 EUR).withCentAmount(170) must be(1.70 EUR) - (1.10 EUR).withCentAmount(1711) must be(17.11 EUR) - (1 JPY).withCentAmount(34) must be(34 JPY) - } - - it("should provide convenient toString") { - (1 JPY).toString must be("1 JPY") - (1.00 EUR).toString must be("1.00 EUR") - (0.10 EUR).toString must be("0.10 EUR") - (0.01 EUR).toString must be("0.01 EUR") - (0.00 EUR).toString must be("0.00 EUR") - (94.5 EUR).toString must be("94.50 EUR") - } - - it("should not fail on toString") { - forAll(DomainObjectsGen.money) { m => - m.toString - } - } - } -} diff --git a/util-3/src/test/scala/ScalaLoggingCompatiblitySpec.scala b/util-3/src/test/scala/ScalaLoggingCompatiblitySpec.scala deleted file mode 100644 index 35de2b23..00000000 --- a/util-3/src/test/scala/ScalaLoggingCompatiblitySpec.scala +++ /dev/null @@ -1,18 +0,0 @@ -import com.typesafe.scalalogging.Logger -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.must.Matchers - -class ScalaLoggingCompatiblitySpec extends AnyFunSpec with Matchers { - - describe("Ensure we skip ScalaLogging 3.9.5, because varargs will not compile under 3.9.5") { - // Github issue about the bug: https://github.com/lightbend-labs/scala-logging/issues/354 - // This test can be removed if it compiles with scala-logging versions bigger than 3.9.5 - object Log extends com.typesafe.scalalogging.StrictLogging { - val log: Logger = logger - } - val list: List[AnyRef] = List("log", "Some more") - - Log.log.warn("Message1", list*) - } - -} diff --git a/util/dependencies.sbt b/util/dependencies.sbt index e1cc3ec7..f0c76af7 100644 --- a/util/dependencies.sbt +++ b/util/dependencies.sbt @@ -2,6 +2,6 @@ libraryDependencies ++= Seq( "com.typesafe.scala-logging" %% "scala-logging" % "3.9.4", "joda-time" % "joda-time" % "2.13.0", "org.joda" % "joda-convert" % "3.0.1", - ("org.typelevel" %% "cats-core" % "2.13.0").cross(CrossVersion.binary), + "org.typelevel" %% "cats-core" % "2.13.0", "org.json4s" %% "json4s-scalap" % "4.0.7" ) From 65573fa13fafedcef6161a7c185318fe01941e7d Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 21 Feb 2025 11:49:23 +0100 Subject: [PATCH 056/142] Formatting --- json/json-3/src/test/scala/io/sphere/json/JSONSpec.scala | 3 ++- .../io/sphere/mongo/catsinstances/catsinstances.scala | 2 +- .../src/main/scala/io/sphere/mongo/generic/generic.scala | 2 +- .../src/test/scala/io/sphere/mongo/DerivationSpec.scala | 7 +------ .../io/sphere/mongo/generic/MongoTypeSwitchSpec.scala | 2 +- 5 files changed, 6 insertions(+), 10 deletions(-) diff --git a/json/json-3/src/test/scala/io/sphere/json/JSONSpec.scala b/json/json-3/src/test/scala/io/sphere/json/JSONSpec.scala index cb12c5e2..0bc584cd 100644 --- a/json/json-3/src/test/scala/io/sphere/json/JSONSpec.scala +++ b/json/json-3/src/test/scala/io/sphere/json/JSONSpec.scala @@ -181,7 +181,8 @@ class JSONSpec extends AnyFunSpec with Matchers { } it("must provide derived instances for singleton objects") { - implicit val singletonJSON: JSON[JSONSpec.Singleton.type] = deriveJSON[JSONSpec.Singleton.type] + implicit val singletonJSON: JSON[JSONSpec.Singleton.type] = + deriveJSON[JSONSpec.Singleton.type] val json = s"""[${toJSON(Singleton)}]""" withClue(json) { diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/catsinstances/catsinstances.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/catsinstances/catsinstances.scala index 1b6d9d4f..d1e9cde6 100644 --- a/mongo/mongo-3/src/main/scala/io/sphere/mongo/catsinstances/catsinstances.scala +++ b/mongo/mongo-3/src/main/scala/io/sphere/mongo/catsinstances/catsinstances.scala @@ -4,7 +4,7 @@ import _root_.cats.Invariant import io.sphere.mongo.format.MongoFormat /** Cats instances for [[MongoFormat]] - */ + */ package object catsinstances extends MongoFormatInstances trait MongoFormatInstances { diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/generic.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/generic.scala index 9eba298b..bc378935 100644 --- a/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/generic.scala +++ b/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/generic.scala @@ -4,7 +4,7 @@ import com.mongodb.BasicDBObject import io.sphere.mongo.format.MongoFormat import org.bson.BSONObject -import scala.compiletime.{erasedValue, summonInline, error} +import scala.compiletime.{erasedValue, error, summonInline} case object generic { inline def mongoTypeSwitch[SuperType, SubTypeTuple <: Tuple](): MongoFormat[SuperType] = diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala index b20c5346..469d79fc 100644 --- a/mongo/mongo-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala @@ -1,11 +1,6 @@ package io.sphere.mongo -import io.sphere.mongo.generic.{ - AnnotationReader, - MongoEmbedded, - MongoKey, - MongoTypeHintField -} +import io.sphere.mongo.generic.{AnnotationReader, MongoEmbedded, MongoKey, MongoTypeHintField} import io.sphere.mongo.format.DefaultMongoFormats.given import org.scalatest.wordspec.AnyWordSpec import org.scalatest.matchers.must.Matchers diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala index 0b6d6090..b2bb0188 100644 --- a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala @@ -13,7 +13,7 @@ class MongoTypeSwitchSpec extends AnyWordSpec with Matchers { case class B(int: Int) extends A case class C(int: Int) extends A @MongoTypeHint("D2") case class D(int: Int) extends A - + "mongoTypeSwitch" must { "derive a subset of a sealed trait" in { val format = generic.mongoTypeSwitch[A, (B, C)]() From d2bd471556e2a6515e67a80c683f724b43d2dc13 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 21 Feb 2025 12:06:53 +0100 Subject: [PATCH 057/142] Trying to make the pipeline work --- build.sbt | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/build.sbt b/build.sbt index d110f21d..8520266b 100644 --- a/build.sbt +++ b/build.sbt @@ -25,8 +25,6 @@ ThisBuild / githubWorkflowBuild := Seq( WorkflowStep.Sbt( commands = List( "sphere-util/test", - "sphere-json-core/test", - "sphere-mongo-core/test", "sphere-mongo-3/test", "sphere-json-3/test" ), @@ -117,6 +115,12 @@ lazy val `sphere-json-3` = project .settings(standardSettings: _*) .dependsOn(`sphere-util`) +lazy val `sphere-mongo-3` = project + .settings(scalaVersion := scala3) + .in(file("./mongo/mongo-3")) + .settings(standardSettings: _*) + .dependsOn(`sphere-util`) + // Scala 2 modules lazy val `sphere-util` = project @@ -128,7 +132,7 @@ lazy val `sphere-util` = project lazy val `sphere-json-core` = project .in(file("./json/json-core")) .settings(standardSettings: _*) - .settings(crossScalaVersions := Seq(scala212, scala213, scala3)) + .settings(crossScalaVersions := Seq(scala212, scala213)) .dependsOn(`sphere-util`) lazy val `sphere-json-derivation` = project @@ -149,7 +153,7 @@ lazy val `sphere-json` = project lazy val `sphere-mongo-core` = project .in(file("./mongo/mongo-core")) .settings(standardSettings: _*) - .settings(crossScalaVersions := Seq(scala212, scala213, scala3)) + .settings(crossScalaVersions := Seq(scala212, scala213)) .dependsOn(`sphere-util`) lazy val `sphere-mongo-derivation` = project @@ -159,12 +163,6 @@ lazy val `sphere-mongo-derivation` = project .settings(crossScalaVersions := Seq(scala212, scala213)) .dependsOn(`sphere-mongo-core`) -lazy val `sphere-mongo-3` = project - .settings(scalaVersion := scala3) - .in(file("./mongo/mongo-3")) - .settings(standardSettings: _*) - .dependsOn(`sphere-util`) - lazy val `sphere-mongo-derivation-magnolia` = project .in(file("./mongo/mongo-derivation-magnolia")) .settings(standardSettings: _*) From 214003f06ca4d1762fae7397eb07ce1837ee08d6 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 21 Feb 2025 12:22:28 +0100 Subject: [PATCH 058/142] Trying to make the pipeline work --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 840cccd5..c4f8b665 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,10 +56,10 @@ jobs: - name: Build Scala 3 project if: matrix.scala == '3.3.5' - run: sbt '++ ${{ matrix.scala }}' sphere-util/test sphere-json-core/test sphere-mongo-core/test + run: sbt '++ ${{ matrix.scala }}' sphere-util/test sphere-mongo-3/test sphere-json-3/test - name: Compress target directories - run: tar cf targets.tar benchmarks/target mongo/target json/json-core/target mongo/mongo-core/target json/target util/target json/json-derivation/target mongo/mongo-derivation-magnolia/target target mongo/mongo-derivation/target project/target + run: tar cf targets.tar benchmarks/target mongo/target json/json-core/target mongo/mongo-core/target json/target util/target mongo/mongo-3/target json/json-derivation/target mongo/mongo-derivation-magnolia/target target json/json-3/target mongo/mongo-derivation/target project/target - name: Upload target directories uses: actions/upload-artifact@v4 From 1b46be7362777f4c3f8f9df66d32c1933798f0d4 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 21 Feb 2025 14:08:05 +0100 Subject: [PATCH 059/142] Trying to make the pipeline work --- .../main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala | 2 +- util/src/main/scala/Money.scala | 1 - util/src/test/scala/DomainObjectsGen.scala | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/mongo/mongo-core/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala b/mongo/mongo-core/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala index 90536a5f..199343fb 100644 --- a/mongo/mongo-core/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala +++ b/mongo/mongo-core/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala @@ -50,7 +50,7 @@ trait DefaultMongoFormats { implicit def optionFormat[@specialized A](implicit f: MongoFormat[A]): MongoFormat[Option[A]] = new MongoFormat[Option[A]] { - import scala.jdk.CollectionConverters._ + import scala.collection.JavaConverters._ override def toMongoValue(a: Option[A]) = a match { case Some(aa) => f.toMongoValue(aa) case None => MongoNothing diff --git a/util/src/main/scala/Money.scala b/util/src/main/scala/Money.scala index 7aa83451..24b47d97 100644 --- a/util/src/main/scala/Money.scala +++ b/util/src/main/scala/Money.scala @@ -263,7 +263,6 @@ object Money { } def apply(amount: BigDecimal, currency: Currency): Money = { - println("this is called") require( amount.scale == currency.getDefaultFractionDigits, "The scale of the given amount does not match the scale of the provided currency." + diff --git a/util/src/test/scala/DomainObjectsGen.scala b/util/src/test/scala/DomainObjectsGen.scala index b654f020..97536bfd 100644 --- a/util/src/test/scala/DomainObjectsGen.scala +++ b/util/src/test/scala/DomainObjectsGen.scala @@ -4,7 +4,7 @@ import java.util.Currency import org.scalacheck.Gen -import scala.jdk.CollectionConverters._ +import scala.collection.JavaConverters._ object DomainObjectsGen { From d290d98a78f1cb16719bb646cf90242c46acf2ba Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 21 Feb 2025 14:24:39 +0100 Subject: [PATCH 060/142] Trying to make the pipeline work --- .../scala/io/sphere/mongo/format/DefaultMongoFormats.scala | 6 +++--- .../io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala | 2 +- .../io/sphere/mongo/format/DefaultMongoFormatsTest.scala | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala index 61dd1915..0c14edd0 100644 --- a/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala +++ b/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala @@ -64,7 +64,7 @@ trait DefaultMongoFormats { given vecFormat[@specialized A](using format: MongoFormat[A]): MongoFormat[Vector[A]] = new MongoFormat[Vector[A]] { - import scala.jdk.CollectionConverters._ + import scala.collection.JavaConverters._ override def toMongoValue(a: Vector[A]) = { val m = new BasicBSONList() if (a.nonEmpty) @@ -90,7 +90,7 @@ trait DefaultMongoFormats { given listFormat[@specialized A](using format: MongoFormat[A]): MongoFormat[List[A]] = new MongoFormat[List[A]] { - import scala.jdk.CollectionConverters._ + import scala.collection.JavaConverters._ override def toMongoValue(a: List[A]) = { val m = new BasicBSONList() if (a.nonEmpty) @@ -116,7 +116,7 @@ trait DefaultMongoFormats { given setFormat[@specialized A](using f: MongoFormat[A]): MongoFormat[Set[A]] = new MongoFormat[Set[A]] { - import scala.jdk.CollectionConverters._ + import scala.collection.JavaConverters._ override def toMongoValue(a: Set[A]) = { val m = new BasicBSONList() if (a.nonEmpty) diff --git a/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala b/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala index 9638fd06..27ab0fd0 100644 --- a/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala +++ b/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala @@ -9,7 +9,7 @@ import org.bson.BSONObject import org.scalatest.wordspec.AnyWordSpec import org.scalatest.matchers.should.Matchers -import scala.jdk.CollectionConverters._ +import scala.collection.JavaConverters._ class BaseMoneyMongoFormatTest extends AnyWordSpec with Matchers { diff --git a/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/DefaultMongoFormatsTest.scala b/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/DefaultMongoFormatsTest.scala index a29bf5c7..f03efc81 100644 --- a/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/DefaultMongoFormatsTest.scala +++ b/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/DefaultMongoFormatsTest.scala @@ -12,7 +12,7 @@ import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks -import scala.jdk.CollectionConverters._ +import scala.collection.JavaConverters._ object DefaultMongoFormatsTest { case class User(name: String) From f1ff3711a212e4ecb29a6e91a9165b68f0d6d19d Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 21 Feb 2025 14:28:08 +0100 Subject: [PATCH 061/142] Trying to make the pipeline work --- build.sbt | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 8520266b..c4431314 100644 --- a/build.sbt +++ b/build.sbt @@ -19,9 +19,20 @@ ThisBuild / githubWorkflowBuildMatrixFailFast := Some(false) // note that `sbt +test` is working fine to run cross-compiled tests locally ThisBuild / githubWorkflowBuild := Seq( WorkflowStep.Sbt( - commands = List("test"), + commands = List( + "sphere-util/test", + "sphere-json/test", + "sphere-json-core/test", + "sphere-json-derivation/test", + "sphere-mongo/test", + "sphere-mongo-core/test", + "sphere-mongo-derivation/test", + "sphere-mongo-derivation-magnolia/test", + "benchmark/test" + ), name = Some("Build Scala 2 project"), - cond = Some(s"matrix.scala != '$scala3'")), + cond = Some(s"matrix.scala != '$scala3'") + ), WorkflowStep.Sbt( commands = List( "sphere-util/test", From eed2c79dcbf98aacca9abb39b5a5dbf82dc4766d Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 21 Feb 2025 14:30:42 +0100 Subject: [PATCH 062/142] Trying to make the pipeline work --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4f8b665..7d35790b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,7 @@ jobs: - name: Build Scala 2 project if: matrix.scala != '3.3.5' - run: sbt '++ ${{ matrix.scala }}' test + run: sbt '++ ${{ matrix.scala }}' sphere-util/test sphere-json/test sphere-json-core/test sphere-json-derivation/test sphere-mongo/test sphere-mongo-core/test sphere-mongo-derivation/test sphere-mongo-derivation-magnolia/test benchmark/test - name: Build Scala 3 project if: matrix.scala == '3.3.5' From c553fc909e5244da08c9a08613852fce864a343f Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 21 Feb 2025 14:35:19 +0100 Subject: [PATCH 063/142] Trying to make the pipeline work --- .github/workflows/ci.yml | 2 +- build.sbt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d35790b..9d2b9e2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,7 @@ jobs: - name: Build Scala 2 project if: matrix.scala != '3.3.5' - run: sbt '++ ${{ matrix.scala }}' sphere-util/test sphere-json/test sphere-json-core/test sphere-json-derivation/test sphere-mongo/test sphere-mongo-core/test sphere-mongo-derivation/test sphere-mongo-derivation-magnolia/test benchmark/test + run: sbt '++ ${{ matrix.scala }}' sphere-util/test sphere-json/test sphere-json-core/test sphere-json-derivation/test sphere-mongo/test sphere-mongo-core/test sphere-mongo-derivation/test sphere-mongo-derivation-magnolia/test benchmarks/test - name: Build Scala 3 project if: matrix.scala == '3.3.5' diff --git a/build.sbt b/build.sbt index c4431314..fc2c6654 100644 --- a/build.sbt +++ b/build.sbt @@ -28,7 +28,7 @@ ThisBuild / githubWorkflowBuild := Seq( "sphere-mongo-core/test", "sphere-mongo-derivation/test", "sphere-mongo-derivation-magnolia/test", - "benchmark/test" + "benchmarks/test" ), name = Some("Build Scala 2 project"), cond = Some(s"matrix.scala != '$scala3'") From 40ab176704c76f5fca2e76f596320a06402c988b Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 21 Feb 2025 14:39:32 +0100 Subject: [PATCH 064/142] Trying to make the pipeline work --- json/json-3/dependencies.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/json/json-3/dependencies.sbt b/json/json-3/dependencies.sbt index 22bf4db4..3f454395 100644 --- a/json/json-3/dependencies.sbt +++ b/json/json-3/dependencies.sbt @@ -1,5 +1,5 @@ libraryDependencies ++= Seq( - ("org.json4s" %% "json4s-jackson" % "4.0.7").cross(CrossVersion.for3Use2_13), + ("org.json4s" %% "json4s-jackson" % "4.0.7").cross(CrossVersion.binary), "com.fasterxml.jackson.core" % "jackson-databind" % "2.17.2", - ("org.typelevel" %% "cats-core" % "2.13.0").cross(CrossVersion.for3Use2_13) + ("org.typelevel" %% "cats-core" % "2.13.0").cross(CrossVersion.binary) ) From d1f850dd07a6a499134746e74094bd561d01b38e Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 21 Feb 2025 17:00:41 +0100 Subject: [PATCH 065/142] Remove useless code --- .../io/sphere/json/generic/Derivation.scala | 124 ------------------ 1 file changed, 124 deletions(-) delete mode 100644 json/json-3/src/main/scala/io/sphere/json/generic/Derivation.scala diff --git a/json/json-3/src/main/scala/io/sphere/json/generic/Derivation.scala b/json/json-3/src/main/scala/io/sphere/json/generic/Derivation.scala deleted file mode 100644 index 805654ea..00000000 --- a/json/json-3/src/main/scala/io/sphere/json/generic/Derivation.scala +++ /dev/null @@ -1,124 +0,0 @@ -//package io.sphere.json.generic -// -//import cats.data.Validated -//import cats.implicits.* -//import io.sphere.json.{JSON, JSONParseError, JValidation} -//import org.json4s.DefaultJsonFormats.given -//import org.json4s.JsonAST.JValue -//import org.json4s.{DefaultJsonFormats, JObject, JString, jvalue2monadic, jvalue2readerSyntax} -// -//import scala.deriving.Mirror -// -//inline def deriveJSON[A](using Mirror.Of[A]): JSON[A] = JSON.derived -// -//object JSON { -// inline def apply[A: JSON]: JSON[A] = summon[JSON[A]] -// inline given derived[A](using Mirror.Of[A]): JSON[A] = Derivation.derived[A] -// -// private def addField(jObject: JObject, field: Field, jValue: JValue): JValue = -// jValue match { -// case o: JObject => -// if (field.embedded) JObject(jObject.obj ++ o.obj) -// else JObject(jObject.obj :+ (field.fieldName -> o)) -// case other => JObject(jObject.obj :+ (field.fieldName -> other)) -// } -// -// private object Derivation { -// -// import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} -// -// inline def derived[A](using m: Mirror.Of[A]): JSON[A] = -// inline m match { -// case s: Mirror.SumOf[A] => deriveTrait(s) -// case p: Mirror.ProductOf[A] => deriveCaseClass(p) -// } -// -// inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): JSON[A] = -// new JSON[A] { -// private val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] -// private val typeHintMap: Map[String, String] = traitMetaData.subtypes.collect { -// case (name, classMeta) if classMeta.typeHint.isDefined => -// name -> classMeta.typeHint.get -// } -// private val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) -// private val jsons: Seq[JSON[Any]] = summonFormatters[mirrorOfSum.MirroredElemTypes] -// private val names: Seq[String] = -// constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector -// .asInstanceOf[Vector[String]] -// private val jsonsByNames: Map[String, JSON[Any]] = names.zip(jsons).toMap -// -// override def read(jValue: JValue): JValidation[A] = -// jValue match { -// case jObject: JObject => -// val typeName = (jObject \ traitMetaData.typeDiscriminator).as[String] -// val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) -// jsonsByNames(originalTypeName).read(jObject).map(_.asInstanceOf[A]) -// case x => -// Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$jValue'")) -// } -// -// override def write(value: A): JValue = { -// // we never get a trait here, only classes, it's safe to assume Product -// val originalTypeName = value.asInstanceOf[Product].productPrefix -// val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) -// val json = jsonsByNames(originalTypeName).write(value).asInstanceOf[JObject] -// val typeDiscriminator = traitMetaData.typeDiscriminator -> JString(typeName) -// JObject(typeDiscriminator :: json.obj) -// } -// -// } -// -// inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): JSON[A] = -// new JSON[A] { -// private val caseClassMetaData: CaseClassMetaData = AnnotationReader.readCaseClassMetaData[A] -// private val jsons: Vector[JSON[Any]] = summonFormatters[mirrorOfProduct.MirroredElemTypes] -// private val fieldsAndJsons: Vector[(Field, JSON[Any])] = caseClassMetaData.fields.zip(jsons) -// -// private val fieldNames: Vector[String] = fieldsAndJsons.flatMap { (field, json) => -// if (field.embedded) json.fields.toVector :+ field.name -// else Vector(field.name) -// } -// -// override val fields: Set[String] = fieldNames.toSet -// -// override def write(value: A): JValue = { -// val caseClassFields = value.asInstanceOf[Product].productIterator -// jsons -// .zip(caseClassFields) -// .zip(caseClassMetaData.fields) -// .foldLeft[JValue](JObject()) { case (jObject, ((json, fieldValue), field)) => -// addField(jObject.asInstanceOf[JObject], field, json.write(fieldValue)) -// } -// } -// -// override def read(jValue: JValue): JValidation[A] = -// jValue match { -// case jObject: JObject => -// for { -// fieldsAsAList <- fieldsAndJsons -// .map((field, format) => readField(field, format, jObject)) -// .sequence -// fieldsAsTuple = Tuple.fromArray(fieldsAsAList.toArray) -// -// } yield mirrorOfProduct.fromTuple( -// fieldsAsTuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) -// -// case x => -// Validated.invalidNel(JSONParseError(s"JSON object expected. $x")) -// } -// -// private def readField(field: Field, json: JSON[Any], jObject: JObject): JValidation[Any] = -// if (field.embedded) json.read(jObject) -// else io.sphere.json.field(field.fieldName, field.defaultArgument)(jObject)(json) -// -// } -// -// inline private def summonFormatters[T <: Tuple]: Vector[JSON[Any]] = -// inline erasedValue[T] match { -// case _: EmptyTuple => Vector.empty -// case _: (t *: ts) => -// summonInline[JSON[t]] -// .asInstanceOf[JSON[Any]] +: summonFormatters[ts] -// } -// } -//} From 7e7c87a8d1a53e1900a82df804f3ac7fa9abc5b7 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 21 Feb 2025 17:03:29 +0100 Subject: [PATCH 066/142] Trying different cross compilation settings --- json/json-3/dependencies.sbt | 4 ++-- util/dependencies.sbt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/json/json-3/dependencies.sbt b/json/json-3/dependencies.sbt index 3f454395..09b36682 100644 --- a/json/json-3/dependencies.sbt +++ b/json/json-3/dependencies.sbt @@ -1,5 +1,5 @@ libraryDependencies ++= Seq( - ("org.json4s" %% "json4s-jackson" % "4.0.7").cross(CrossVersion.binary), + "org.json4s" %% "json4s-jackson" % "4.0.7", "com.fasterxml.jackson.core" % "jackson-databind" % "2.17.2", - ("org.typelevel" %% "cats-core" % "2.13.0").cross(CrossVersion.binary) + "org.typelevel" %% "cats-core" % "2.13.0" ) diff --git a/util/dependencies.sbt b/util/dependencies.sbt index f0c76af7..12f3098d 100644 --- a/util/dependencies.sbt +++ b/util/dependencies.sbt @@ -2,6 +2,6 @@ libraryDependencies ++= Seq( "com.typesafe.scala-logging" %% "scala-logging" % "3.9.4", "joda-time" % "joda-time" % "2.13.0", "org.joda" % "joda-convert" % "3.0.1", - "org.typelevel" %% "cats-core" % "2.13.0", - "org.json4s" %% "json4s-scalap" % "4.0.7" + ("org.typelevel" %% "cats-core" % "2.13.0").cross(CrossVersion.binary), + ("org.json4s" %% "json4s-scalap" % "4.0.7").cross(CrossVersion.binary) ) From c43a36eddc4c7aeda3f27a2feafcfe95c0858cb3 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 21 Feb 2025 17:18:18 +0100 Subject: [PATCH 067/142] Fix some package names/imports --- .../sphere/json/generic/JsonTypeSwitch.scala | 78 ------------- .../io/sphere/json/generic/generic.scala | 63 +++++++++++ .../json/generic/JsonTypeSwitchSpec.scala | 4 +- .../io/sphere/mongo/generic/generic.scala | 106 ++++++++---------- .../mongo/generic/MongoTypeSwitchSpec.scala | 4 +- 5 files changed, 113 insertions(+), 142 deletions(-) delete mode 100644 json/json-3/src/main/scala/io/sphere/json/generic/JsonTypeSwitch.scala create mode 100644 json/json-3/src/main/scala/io/sphere/json/generic/generic.scala diff --git a/json/json-3/src/main/scala/io/sphere/json/generic/JsonTypeSwitch.scala b/json/json-3/src/main/scala/io/sphere/json/generic/JsonTypeSwitch.scala deleted file mode 100644 index 385f7e90..00000000 --- a/json/json-3/src/main/scala/io/sphere/json/generic/JsonTypeSwitch.scala +++ /dev/null @@ -1,78 +0,0 @@ -package io.sphere.json.generic - -import cats.data.Validated -import io.sphere.json.{JSON, JSONParseError, JValidation} -import org.json4s.DefaultJsonFormats.given -import org.json4s.{JObject, JString, jvalue2monadic, jvalue2readerSyntax} -import org.json4s.JsonAST.JValue - -import scala.deriving.Mirror - -object JsonTypeSwitch { - import scala.compiletime.{erasedValue, error, summonInline} - - inline def jsonTypeSwitch[SuperType, SubTypeTuple <: Tuple](): JSON[SuperType] = - new JSON[SuperType] { - private val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] - private val typeHintMap = traitMetaData.subTypeTypeHints - private val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) - private val formattersAndMetaData: Vector[(TraitMetaData, JSON[Any])] = - summonFormatters[SubTypeTuple]() - - // Separate Trait formatters from CaseClass formatters, so we can avoid adding the typeDiscriminator twice - private val (traitFormatterList, caseClassFormatterList) = - formattersAndMetaData.partitionMap { (meta, formatter) => - if (meta.isTrait) - Left(meta.subtypes.map(_._2.name -> formatter)) - else - Right(meta.top.name -> formatter) - } - val traitFormatters = traitFormatterList.flatten.toMap - val caseClassFormatters = caseClassFormatterList.toMap - val allFormattersByTypeName = traitFormatters ++ caseClassFormatters - - override def write(a: SuperType): JValue = { - val originalTypeName = a.asInstanceOf[Product].productPrefix - val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) - val traitFormatterOpt = traitFormatters.get(originalTypeName) - traitFormatterOpt - .map(_.write(a).asInstanceOf[JObject]) - .getOrElse { - val json = caseClassFormatters(originalTypeName).write(a).asInstanceOf[JObject] - val typeDiscriminator = traitMetaData.typeDiscriminator -> JString(typeName) - JObject(typeDiscriminator :: json.obj) - } - } - - override def read(jValue: JValue): JValidation[SuperType] = - jValue match { - case jObject: JObject => - val typeName = (jObject \ traitMetaData.typeDiscriminator).as[String] - val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - allFormattersByTypeName(originalTypeName).read(jObject).map(_.asInstanceOf[SuperType]) - case x => - Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$jValue'")) - } - } - - inline private def failIfAnySubTypeIsNotAProduct[T <: Tuple]: Unit = - inline erasedValue[T] match { - case _: EmptyTuple => () - case _: (t *: ts) => - inline erasedValue[t] match { - case _: Product => failIfAnySubTypeIsNotAProduct[ts] - case _ => error("All types should be subtypes of Product") - } - } - - inline private def summonFormatters[T <: Tuple]( - acc: Vector[(TraitMetaData, JSON[Any])] = Vector.empty): Vector[(TraitMetaData, JSON[Any])] = - inline erasedValue[T] match { - case _: EmptyTuple => acc - case _: (t *: ts) => - val traitMetaData = AnnotationReader.readTraitMetaData[t] - val headFormatter = summonInline[JSON[t]].asInstanceOf[JSON[Any]] - summonFormatters[ts](acc :+ (traitMetaData -> headFormatter)) - } - -} diff --git a/json/json-3/src/main/scala/io/sphere/json/generic/generic.scala b/json/json-3/src/main/scala/io/sphere/json/generic/generic.scala new file mode 100644 index 00000000..f6a83979 --- /dev/null +++ b/json/json-3/src/main/scala/io/sphere/json/generic/generic.scala @@ -0,0 +1,63 @@ +package io.sphere.json.generic + +import cats.data.Validated +import io.sphere.json.{JSON, JSONParseError, JValidation} +import org.json4s.DefaultJsonFormats.given +import org.json4s.{JObject, JString, jvalue2monadic, jvalue2readerSyntax} +import org.json4s.JsonAST.JValue + +import scala.compiletime.{erasedValue, error, summonInline} + +inline def jsonTypeSwitch[SuperType, SubTypeTuple <: Tuple](): JSON[SuperType] = + new JSON[SuperType] { + private val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] + private val typeHintMap = traitMetaData.subTypeTypeHints + private val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) + private val formattersAndMetaData: Vector[(TraitMetaData, JSON[Any])] = + summonFormatters[SubTypeTuple]() + + // Separate Trait formatters from CaseClass formatters, so we can avoid adding the typeDiscriminator twice + private val (traitFormatterList, caseClassFormatterList) = + formattersAndMetaData.partitionMap { (meta, formatter) => + if (meta.isTrait) + Left(meta.subtypes.map(_._2.name -> formatter)) + else + Right(meta.top.name -> formatter) + } + val traitFormatters = traitFormatterList.flatten.toMap + val caseClassFormatters = caseClassFormatterList.toMap + val allFormattersByTypeName = traitFormatters ++ caseClassFormatters + + override def write(a: SuperType): JValue = { + val originalTypeName = a.asInstanceOf[Product].productPrefix + val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) + val traitFormatterOpt = traitFormatters.get(originalTypeName) + traitFormatterOpt + .map(_.write(a).asInstanceOf[JObject]) + .getOrElse { + val json = caseClassFormatters(originalTypeName).write(a).asInstanceOf[JObject] + val typeDiscriminator = traitMetaData.typeDiscriminator -> JString(typeName) + JObject(typeDiscriminator :: json.obj) + } + } + + override def read(jValue: JValue): JValidation[SuperType] = + jValue match { + case jObject: JObject => + val typeName = (jObject \ traitMetaData.typeDiscriminator).as[String] + val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) + allFormattersByTypeName(originalTypeName).read(jObject).map(_.asInstanceOf[SuperType]) + case x => + Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$jValue'")) + } + } + +inline private def summonFormatters[T <: Tuple]( + acc: Vector[(TraitMetaData, JSON[Any])] = Vector.empty): Vector[(TraitMetaData, JSON[Any])] = + inline erasedValue[T] match { + case _: EmptyTuple => acc + case _: (t *: ts) => + val traitMetaData = AnnotationReader.readTraitMetaData[t] + val headFormatter = summonInline[JSON[t]].asInstanceOf[JSON[Any]] + summonFormatters[ts](acc :+ (traitMetaData -> headFormatter)) + } diff --git a/json/json-3/src/test/scala/io/sphere/json/generic/JsonTypeSwitchSpec.scala b/json/json-3/src/test/scala/io/sphere/json/generic/JsonTypeSwitchSpec.scala index 1ad3d5cc..e9bacb0d 100644 --- a/json/json-3/src/test/scala/io/sphere/json/generic/JsonTypeSwitchSpec.scala +++ b/json/json-3/src/test/scala/io/sphere/json/generic/JsonTypeSwitchSpec.scala @@ -2,7 +2,7 @@ package io.sphere.json.generic import cats.data.Validated.Valid import io.sphere.json.{JSON, deriveJSON} -import io.sphere.json.generic.JsonTypeSwitch.jsonTypeSwitch +import io.sphere.json.generic.jsonTypeSwitch import org.json4s.JsonAST.JObject import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -82,7 +82,7 @@ object JsonTypeSwitchSpec { // this can be dangerous is the same class name is used in both sum types // ex if we define TypeA.Class1 && TypeB.Class1 // as both will use the same type value discriminator - implicit val json: JSON[Message] = JsonTypeSwitch.jsonTypeSwitch[Message, (TypeA, TypeB)]() + implicit val json: JSON[Message] = jsonTypeSwitch[Message, (TypeA, TypeB)]() } sealed trait TypeA extends Message diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/generic.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/generic.scala index bc378935..362d426e 100644 --- a/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/generic.scala +++ b/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/generic.scala @@ -6,65 +6,51 @@ import org.bson.BSONObject import scala.compiletime.{erasedValue, error, summonInline} -case object generic { - inline def mongoTypeSwitch[SuperType, SubTypeTuple <: Tuple](): MongoFormat[SuperType] = - new MongoFormat[SuperType] { - failIfAnySubTypeIsNotAProduct[SubTypeTuple] - private val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] - private val typeHintMap = traitMetaData.subTypeTypeHints - private val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) - private val formatters: Vector[MongoFormat[Any]] = summonFormatters[SubTypeTuple]() - private val names = summonMetaData[SubTypeTuple]().map(_.name) - private val formattersByTypeName = names.zip(formatters).toMap - - override def toMongoValue(a: SuperType): Any = { - val originalTypeName = a.asInstanceOf[Product].productPrefix - val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) - val bson = - formattersByTypeName(originalTypeName).toMongoValue(a).asInstanceOf[BasicDBObject] - bson.put(traitMetaData.typeDiscriminator, typeName) - bson - } - - override def fromMongoValue(bson: Any): SuperType = - bson match { - case bson: BasicDBObject => - val typeName = bson.get(traitMetaData.typeDiscriminator).asInstanceOf[String] - val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - formattersByTypeName(originalTypeName).fromMongoValue(bson).asInstanceOf[SuperType] - case x => - throw new Exception(s"BsonObject is expected for a Trait subtype, instead got $x") - } - } - - private def findTypeValue(dbo: BSONObject, typeField: String): Option[String] = - Option(dbo.get(typeField)).map(_.toString) - - inline private def failIfAnySubTypeIsNotAProduct[T <: Tuple]: Unit = - inline erasedValue[T] match { - case _: EmptyTuple => () - case _: (t *: ts) => - inline erasedValue[t] match { - case _: Product => failIfAnySubTypeIsNotAProduct[ts] - case _ => error("All types should be subtypes of Product") - } +inline def mongoTypeSwitch[SuperType, SubTypeTuple <: Tuple](): MongoFormat[SuperType] = + new MongoFormat[SuperType] { + private val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] + private val typeHintMap = traitMetaData.subTypeTypeHints + private val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) + private val formatters: Vector[MongoFormat[Any]] = summonFormatters[SubTypeTuple]() + private val names = summonMetaData[SubTypeTuple]().map(_.name) + private val formattersByTypeName = names.zip(formatters).toMap + + override def toMongoValue(a: SuperType): Any = { + val originalTypeName = a.asInstanceOf[Product].productPrefix + val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) + val bson = + formattersByTypeName(originalTypeName).toMongoValue(a).asInstanceOf[BasicDBObject] + bson.put(traitMetaData.typeDiscriminator, typeName) + bson } - inline private def summonMetaData[T <: Tuple]( - acc: Vector[CaseClassMetaData] = Vector.empty): Vector[CaseClassMetaData] = - inline erasedValue[T] match { - case _: EmptyTuple => acc - case _: (t *: ts) => - summonMetaData[ts](acc :+ AnnotationReader.readCaseClassMetaData[t]) - } - - inline private def summonFormatters[T <: Tuple]( - acc: Vector[MongoFormat[Any]] = Vector.empty): Vector[MongoFormat[Any]] = - inline erasedValue[T] match { - case _: EmptyTuple => acc - case _: (t *: ts) => - val headFormatter = summonInline[MongoFormat[t]].asInstanceOf[MongoFormat[Any]] - summonFormatters[ts](acc :+ headFormatter) - } - -} + override def fromMongoValue(bson: Any): SuperType = + bson match { + case bson: BasicDBObject => + val typeName = bson.get(traitMetaData.typeDiscriminator).asInstanceOf[String] + val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) + formattersByTypeName(originalTypeName).fromMongoValue(bson).asInstanceOf[SuperType] + case x => + throw new Exception(s"BsonObject is expected for a Trait subtype, instead got $x") + } + } + +private def findTypeValue(dbo: BSONObject, typeField: String): Option[String] = + Option(dbo.get(typeField)).map(_.toString) + +inline private def summonMetaData[T <: Tuple]( + acc: Vector[CaseClassMetaData] = Vector.empty): Vector[CaseClassMetaData] = + inline erasedValue[T] match { + case _: EmptyTuple => acc + case _: (t *: ts) => + summonMetaData[ts](acc :+ AnnotationReader.readCaseClassMetaData[t]) + } + +inline private def summonFormatters[T <: Tuple]( + acc: Vector[MongoFormat[Any]] = Vector.empty): Vector[MongoFormat[Any]] = + inline erasedValue[T] match { + case _: EmptyTuple => acc + case _: (t *: ts) => + val headFormatter = summonInline[MongoFormat[t]].asInstanceOf[MongoFormat[Any]] + summonFormatters[ts](acc :+ headFormatter) + } diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala index b2bb0188..56bda822 100644 --- a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala +++ b/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala @@ -16,7 +16,7 @@ class MongoTypeSwitchSpec extends AnyWordSpec with Matchers { "mongoTypeSwitch" must { "derive a subset of a sealed trait" in { - val format = generic.mongoTypeSwitch[A, (B, C)]() + val format = mongoTypeSwitch[A, (B, C)]() val b = B(123) val bson = format.toMongoValue(b) @@ -34,7 +34,7 @@ class MongoTypeSwitchSpec extends AnyWordSpec with Matchers { } "derive a subset of a sealed trait with a mongoKey" in { - val format = generic.mongoTypeSwitch[A, (B, D)]() + val format = mongoTypeSwitch[A, (B, D)]() val d = D(123) val bson = format.toMongoValue(d).asInstanceOf[BSONObject] From ac356277eefe992c63c03553087d1c781fad89cf Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 21 Feb 2025 17:37:49 +0100 Subject: [PATCH 068/142] Trying different cross compilation settings --- util/dependencies.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/util/dependencies.sbt b/util/dependencies.sbt index 12f3098d..26759f84 100644 --- a/util/dependencies.sbt +++ b/util/dependencies.sbt @@ -2,6 +2,6 @@ libraryDependencies ++= Seq( "com.typesafe.scala-logging" %% "scala-logging" % "3.9.4", "joda-time" % "joda-time" % "2.13.0", "org.joda" % "joda-convert" % "3.0.1", - ("org.typelevel" %% "cats-core" % "2.13.0").cross(CrossVersion.binary), - ("org.json4s" %% "json4s-scalap" % "4.0.7").cross(CrossVersion.binary) + ("org.typelevel" %% "cats-core" % "2.13.0"), + ("org.json4s" %% "json4s-scalap" % "4.0.7") ) From 563ddbe1e485776b9911bb3ea5576543f17a05de Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 11 Apr 2025 14:08:25 +0200 Subject: [PATCH 069/142] 1. removing redundant directory (json-derivation-scala-3) 2. moving files to json-core from scala3 --- build.sbt | 9 +- json/json-3/dependencies.sbt | 5 - .../io/sphere/json/FromJSON.scala | 0 .../io/sphere/json/JSON.scala | 0 .../io/sphere/json/ToJSON.scala | 0 .../scala-3/io.sphere.json}/FromJSON.scala | 26 +- .../main/scala-3/io.sphere.json}/JSON.scala | 0 .../main/scala-3/io.sphere.json}/ToJSON.scala | 0 .../generic/AnnotationReader.scala | 0 .../io.sphere.json}/generic/Annotations.scala | 0 .../generic/DeriveSingleton.scala | 0 .../io.sphere.json}/generic/generic.scala | 0 .../json/generic/AnnotationReader.scala | 153 ------- .../io/sphere/json/generic/Annotations.scala | 11 - .../io/sphere/json/generic/Derivation.scala | 126 ------ .../sphere/json/generic/DeriveSingleton.scala | 82 ---- .../sphere/json/DeriveSingletonJSONSpec.scala | 171 -------- .../io/sphere/json/JSONEmbeddedSpec.scala | 131 ------ .../test/scala/io/sphere/json/JSONSpec.scala | 391 ------------------ .../io/sphere/json/NullHandlingSpec.scala | 68 --- .../io/sphere/json/OptionReaderSpec.scala | 150 ------- .../io/sphere/json/TypesSwitchSpec.scala | 87 ---- .../json/generic/DefaultValuesSpec.scala | 44 -- .../io/sphere/json/generic/JSONKeySpec.scala | 47 --- .../json/generic/JsonTypeHintFieldSpec.scala | 63 --- 25 files changed, 24 insertions(+), 1540 deletions(-) delete mode 100644 json/json-3/dependencies.sbt rename json/json-core/src/main/{scala => scala-2}/io/sphere/json/FromJSON.scala (100%) rename json/json-core/src/main/{scala => scala-2}/io/sphere/json/JSON.scala (100%) rename json/json-core/src/main/{scala => scala-2}/io/sphere/json/ToJSON.scala (100%) rename json/{json-3/src/main/scala/io/sphere/json => json-core/src/main/scala-3/io.sphere.json}/FromJSON.scala (95%) rename json/{json-3/src/main/scala/io/sphere/json => json-core/src/main/scala-3/io.sphere.json}/JSON.scala (100%) rename json/{json-3/src/main/scala/io/sphere/json => json-core/src/main/scala-3/io.sphere.json}/ToJSON.scala (100%) rename json/{json-3/src/main/scala/io/sphere/json => json-core/src/main/scala-3/io.sphere.json}/generic/AnnotationReader.scala (100%) rename json/{json-3/src/main/scala/io/sphere/json => json-core/src/main/scala-3/io.sphere.json}/generic/Annotations.scala (100%) rename json/{json-3/src/main/scala/io/sphere/json => json-core/src/main/scala-3/io.sphere.json}/generic/DeriveSingleton.scala (100%) rename json/{json-3/src/main/scala/io/sphere/json => json-core/src/main/scala-3/io.sphere.json}/generic/generic.scala (100%) delete mode 100644 json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala delete mode 100644 json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Annotations.scala delete mode 100644 json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Derivation.scala delete mode 100644 json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/DeriveSingleton.scala delete mode 100644 json/json-derivation-scala-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala delete mode 100644 json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala delete mode 100644 json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala delete mode 100644 json/json-derivation-scala-3/src/test/scala/io/sphere/json/NullHandlingSpec.scala delete mode 100644 json/json-derivation-scala-3/src/test/scala/io/sphere/json/OptionReaderSpec.scala delete mode 100644 json/json-derivation-scala-3/src/test/scala/io/sphere/json/TypesSwitchSpec.scala delete mode 100644 json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala delete mode 100644 json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JSONKeySpec.scala delete mode 100644 json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala diff --git a/build.sbt b/build.sbt index fc2c6654..f01f45d8 100644 --- a/build.sbt +++ b/build.sbt @@ -103,7 +103,6 @@ lazy val `sphere-libs` = project .settings(publishArtifact := false, publish := {}, crossScalaVersions := Seq()) .aggregate( // Scala 3 modules - `sphere-json-3`, `sphere-mongo-3`, // Scala 2 modules @@ -120,12 +119,6 @@ lazy val `sphere-libs` = project // Scala 3 modules -lazy val `sphere-json-3` = project - .in(file("./json/json-3")) - .settings(scalaVersion := scala3) - .settings(standardSettings: _*) - .dependsOn(`sphere-util`) - lazy val `sphere-mongo-3` = project .settings(scalaVersion := scala3) .in(file("./mongo/mongo-3")) @@ -143,7 +136,7 @@ lazy val `sphere-util` = project lazy val `sphere-json-core` = project .in(file("./json/json-core")) .settings(standardSettings: _*) - .settings(crossScalaVersions := Seq(scala212, scala213)) + .settings(crossScalaVersions := Seq(scala212, scala213, scala3)) .dependsOn(`sphere-util`) lazy val `sphere-json-derivation` = project diff --git a/json/json-3/dependencies.sbt b/json/json-3/dependencies.sbt deleted file mode 100644 index 09b36682..00000000 --- a/json/json-3/dependencies.sbt +++ /dev/null @@ -1,5 +0,0 @@ -libraryDependencies ++= Seq( - "org.json4s" %% "json4s-jackson" % "4.0.7", - "com.fasterxml.jackson.core" % "jackson-databind" % "2.17.2", - "org.typelevel" %% "cats-core" % "2.13.0" -) diff --git a/json/json-core/src/main/scala/io/sphere/json/FromJSON.scala b/json/json-core/src/main/scala-2/io/sphere/json/FromJSON.scala similarity index 100% rename from json/json-core/src/main/scala/io/sphere/json/FromJSON.scala rename to json/json-core/src/main/scala-2/io/sphere/json/FromJSON.scala diff --git a/json/json-core/src/main/scala/io/sphere/json/JSON.scala b/json/json-core/src/main/scala-2/io/sphere/json/JSON.scala similarity index 100% rename from json/json-core/src/main/scala/io/sphere/json/JSON.scala rename to json/json-core/src/main/scala-2/io/sphere/json/JSON.scala diff --git a/json/json-core/src/main/scala/io/sphere/json/ToJSON.scala b/json/json-core/src/main/scala-2/io/sphere/json/ToJSON.scala similarity index 100% rename from json/json-core/src/main/scala/io/sphere/json/ToJSON.scala rename to json/json-core/src/main/scala-2/io/sphere/json/ToJSON.scala diff --git a/json/json-3/src/main/scala/io/sphere/json/FromJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala similarity index 95% rename from json/json-3/src/main/scala/io/sphere/json/FromJSON.scala rename to json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala index b6473466..c8a58725 100644 --- a/json/json-3/src/main/scala/io/sphere/json/FromJSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala @@ -488,8 +488,10 @@ object FromJSON extends FromJSONInstances { .appendPattern("'T'[HH[:mm[:ss]]]") .appendFraction(time.temporal.ChronoField.NANO_OF_SECOND, 0, 9, true) .optionalStart() - .appendOffset("+HHmm", "Z") + .appendOffset("+HH:MM", "Z") .optionalEnd() + .optionalStart() + .appendOffset("+HHmm", "Z") .optionalEnd() .parseDefaulting(time.temporal.ChronoField.MONTH_OF_YEAR, 1L) .parseDefaulting(time.temporal.ChronoField.DAY_OF_MONTH, 1L) @@ -500,6 +502,24 @@ object FromJSON extends FromJSONInstances { .parseDefaulting(time.temporal.ChronoField.OFFSET_SECONDS, 0L) .toFormatter() + private val lenientLocalDateParser = + new time.format.DateTimeFormatterBuilder() + .optionalStart() + .appendLiteral('+') + .optionalEnd() + .appendValue(time.temporal.ChronoField.YEAR, 1, 9, java.time.format.SignStyle.NORMAL) + .optionalStart() + .appendLiteral('-') + .appendValue(time.temporal.ChronoField.MONTH_OF_YEAR, 1, 2, java.time.format.SignStyle.NORMAL) + .optionalStart() + .appendLiteral('-') + .appendValue(time.temporal.ChronoField.DAY_OF_MONTH, 1, 2, java.time.format.SignStyle.NORMAL) + .optionalEnd() + .optionalEnd() + .parseDefaulting(time.temporal.ChronoField.MONTH_OF_YEAR, 1L) + .parseDefaulting(time.temporal.ChronoField.DAY_OF_MONTH, 1L) + .toFormatter() + implicit val javaInstantReader: FromJSON[time.Instant] = jsonStringReader("Failed to parse date/time: %s")(s => time.Instant.from(lenientInstantParser.parse(s))) @@ -509,8 +529,8 @@ object FromJSON extends FromJSONInstances { time.LocalTime.parse(_, time.format.DateTimeFormatter.ISO_LOCAL_TIME)) implicit val javaLocalDateReader: FromJSON[time.LocalDate] = - jsonStringReader("Failed to parse date: %s")( - time.LocalDate.parse(_, time.format.DateTimeFormatter.ISO_LOCAL_DATE)) + jsonStringReader("Failed to parse date: %s")(s => + time.LocalDate.from(lenientLocalDateParser.parse(s))) implicit val javaYearMonthReader: FromJSON[time.YearMonth] = jsonStringReader("Failed to parse year/month: %s")( diff --git a/json/json-3/src/main/scala/io/sphere/json/JSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/JSON.scala similarity index 100% rename from json/json-3/src/main/scala/io/sphere/json/JSON.scala rename to json/json-core/src/main/scala-3/io.sphere.json/JSON.scala diff --git a/json/json-3/src/main/scala/io/sphere/json/ToJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/ToJSON.scala similarity index 100% rename from json/json-3/src/main/scala/io/sphere/json/ToJSON.scala rename to json/json-core/src/main/scala-3/io.sphere.json/ToJSON.scala diff --git a/json/json-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/AnnotationReader.scala similarity index 100% rename from json/json-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala rename to json/json-core/src/main/scala-3/io.sphere.json/generic/AnnotationReader.scala diff --git a/json/json-3/src/main/scala/io/sphere/json/generic/Annotations.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/Annotations.scala similarity index 100% rename from json/json-3/src/main/scala/io/sphere/json/generic/Annotations.scala rename to json/json-core/src/main/scala-3/io.sphere.json/generic/Annotations.scala diff --git a/json/json-3/src/main/scala/io/sphere/json/generic/DeriveSingleton.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala similarity index 100% rename from json/json-3/src/main/scala/io/sphere/json/generic/DeriveSingleton.scala rename to json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala diff --git a/json/json-3/src/main/scala/io/sphere/json/generic/generic.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala similarity index 100% rename from json/json-3/src/main/scala/io/sphere/json/generic/generic.scala rename to json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala diff --git a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala deleted file mode 100644 index 69c64576..00000000 --- a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/AnnotationReader.scala +++ /dev/null @@ -1,153 +0,0 @@ -package io.sphere.json.generic - -import io.sphere.json.generic.JSONAnnotation -import io.sphere.json.generic.JSONTypeHint - -import scala.quoted.{Expr, Quotes, Type, Varargs} - -private type MA = JSONAnnotation - -case class Field( - name: String, - embedded: Boolean, - ignored: Boolean, - jsonKey: Option[JSONKey], - defaultArgument: Option[Any]) { - val fieldName: String = jsonKey.map(_.value).getOrElse(name) -} - -case class CaseClassMetaData( - name: String, - typeHintRaw: Option[JSONTypeHint], - fields: Vector[Field] -) { - val typeHint: Option[String] = - typeHintRaw.map(_.value).filterNot(_.toList.forall(_ == ' ')) -} - -case class TraitMetaData( - top: CaseClassMetaData, - typeHintFieldRaw: Option[JSONTypeHintField], - subtypes: Map[String, CaseClassMetaData] -) { - val typeDiscriminator: String = typeHintFieldRaw.map(_.value).getOrElse("type") -} - -class AnnotationReader(using q: Quotes) { - - import q.reflect.* - - def readCaseClassMetaData[T: Type]: Expr[CaseClassMetaData] = { - val sym = TypeRepr.of[T].typeSymbol - caseClassMetaData(sym) - } - - def readTraitMetaData[T: Type]: Expr[TraitMetaData] = { - val sym = TypeRepr.of[T].typeSymbol - val typeHintField = - sym.annotations.map(findJSONTypeHintField).find(_.isDefined).flatten match { - case Some(thf) => '{ Some($thf) } - case None => '{ None } - } - - '{ - TraitMetaData( - top = ${ caseClassMetaData(sym) }, - typeHintFieldRaw = $typeHintField, - subtypes = ${ subtypeAnnotations(sym) } - ) - } - } - - private def annotationTree(tree: Tree): Option[Expr[MA]] = - Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[MA]).map(_.asExprOf[MA]) - - private def findEmbedded(tree: Tree): Boolean = - Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[JSONEmbedded]).isDefined - - private def findIgnored(tree: Tree): Boolean = - Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[JSONIgnore]).isDefined - - private def findKey(tree: Tree): Option[Expr[JSONKey]] = - Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[JSONKey]).map(_.asExprOf[JSONKey]) - - private def findTypeHint(tree: Tree): Option[Expr[JSONTypeHint]] = - Option - .when(tree.isExpr)(tree.asExpr) - .filter(_.isExprOf[JSONTypeHint]) - .map(_.asExprOf[JSONTypeHint]) - - private def findJSONTypeHintField(tree: Tree): Option[Expr[JSONTypeHintField]] = - Option - .when(tree.isExpr)(tree.asExpr) - .filter(_.isExprOf[JSONTypeHintField]) - .map(_.asExprOf[JSONTypeHintField]) - - private def collectFieldInfo(companion: Symbol)(s: Symbol, paramIdx: Int): Expr[Field] = { - val embedded = Expr(s.annotations.exists(findEmbedded)) - val ignored = Expr(s.annotations.exists(findIgnored)) - val name = Expr(s.name) - val key = s.annotations.map(findKey).find(_.isDefined).flatten match { - case Some(k) => '{ Some($k) } - case None => '{ None } - } - val defArgOpt = companion - .methodMember(s"$$lessinit$$greater$$default$$${paramIdx + 1}") - .headOption - .map(dm => Ref(dm).asExprOf[Any]) match { - case Some(k) => '{ Some($k) } - case None => '{ None } - } - - '{ - Field( - name = $name, - embedded = $embedded, - ignored = $ignored, - jsonKey = $key, - defaultArgument = $defArgOpt) - } - } - - private def caseClassMetaData(sym: Symbol): Expr[CaseClassMetaData] = { - val caseParams = sym.primaryConstructor.paramSymss.take(1).flatten - val fields = Varargs(caseParams.zipWithIndex.map(collectFieldInfo(sym.companionModule))) - val name = Expr(sym.name) - val typeHint = sym.annotations.map(findTypeHint).find(_.isDefined).flatten match { - case Some(th) => '{ Some($th) } - case None => '{ None } - } - - '{ - CaseClassMetaData( - name = $name, - typeHintRaw = $typeHint, - fields = Vector($fields*) - ) - } - } - - private def subtypeAnnotation(sym: Symbol): Expr[(String, CaseClassMetaData)] = { - val name = Expr(sym.name) - val annots = caseClassMetaData(sym) - '{ ($name, $annots) } - } - - private def subtypeAnnotations(sym: Symbol): Expr[Map[String, CaseClassMetaData]] = { - val subtypes = Varargs(sym.children.map(subtypeAnnotation)) - '{ Map($subtypes*) } - } - -} - -object AnnotationReader { - inline def readCaseClassMetaData[T]: CaseClassMetaData = ${ readCaseClassMetaDataImpl[T] } - - inline def readTraitMetaData[T]: TraitMetaData = ${ readTraitMetaDataImpl[T] } - - private def readCaseClassMetaDataImpl[T: Type](using Quotes): Expr[CaseClassMetaData] = - AnnotationReader().readCaseClassMetaData[T] - - private def readTraitMetaDataImpl[T: Type](using Quotes): Expr[TraitMetaData] = - AnnotationReader().readTraitMetaData[T] -} diff --git a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Annotations.scala b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Annotations.scala deleted file mode 100644 index 7d3ace8d..00000000 --- a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Annotations.scala +++ /dev/null @@ -1,11 +0,0 @@ -package io.sphere.json.generic - -import scala.annotation.StaticAnnotation - -sealed trait JSONAnnotation extends StaticAnnotation - -case class JSONEmbedded() extends JSONAnnotation -case class JSONIgnore() extends JSONAnnotation -case class JSONKey(value: String) extends JSONAnnotation -case class JSONTypeHintField(value: String) extends JSONAnnotation -case class JSONTypeHint(value: String) extends JSONAnnotation diff --git a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Derivation.scala b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Derivation.scala deleted file mode 100644 index 67a26666..00000000 --- a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/Derivation.scala +++ /dev/null @@ -1,126 +0,0 @@ -package io.sphere.json.generic - -import cats.data.Validated -import cats.implicits.* -import io.sphere.json.{JSON, JSONParseError, JValidation} -import org.json4s.DefaultJsonFormats.given -import org.json4s.JsonAST.JValue -import org.json4s.{DefaultJsonFormats, JObject, JString, jvalue2monadic, jvalue2readerSyntax} - -import scala.deriving.Mirror - -inline def deriveJSON[A](using Mirror.Of[A]): JSON[A] = JSON.derived - -object JSON { - private val emptyFieldsSet: Vector[String] = Vector.empty - - inline def apply[A: JSON]: JSON[A] = summon[JSON[A]] - inline given derived[A](using Mirror.Of[A]): JSON[A] = Derivation.derived[A] - - private def addField(jObject: JObject, field: Field, jValue: JValue): JValue = - jValue match { - case o: JObject => - if (field.embedded) JObject(jObject.obj ++ o.obj) - else JObject(jObject.obj :+ (field.fieldName -> o)) - case other => JObject(jObject.obj :+ (field.fieldName -> other)) - } - - private object Derivation { - - import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} - - inline def derived[A](using m: Mirror.Of[A]): JSON[A] = - inline m match { - case s: Mirror.SumOf[A] => deriveTrait(s) - case p: Mirror.ProductOf[A] => deriveCaseClass(p) - } - - inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): JSON[A] = - new JSON[A] { - private val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] - private val typeHintMap: Map[String, String] = traitMetaData.subtypes.collect { - case (name, classMeta) if classMeta.typeHint.isDefined => - name -> classMeta.typeHint.get - } - private val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) - private val jsons: Seq[JSON[Any]] = summonFormatters[mirrorOfSum.MirroredElemTypes] - private val names: Seq[String] = - constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector - .asInstanceOf[Vector[String]] - private val jsonsByNames: Map[String, JSON[Any]] = names.zip(jsons).toMap - - override def read(jValue: JValue): JValidation[A] = - jValue match { - case jObject: JObject => - val typeName = (jObject \ traitMetaData.typeDiscriminator).as[String] - val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - jsonsByNames(originalTypeName).read(jObject).map(_.asInstanceOf[A]) - case x => - Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$jValue'")) - } - - override def write(value: A): JValue = { - // we never get a trait here, only classes, it's safe to assume Product - val originalTypeName = value.asInstanceOf[Product].productPrefix - val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) - val json = jsonsByNames(originalTypeName).write(value).asInstanceOf[JObject] - val typeDiscriminator = traitMetaData.typeDiscriminator -> JString(typeName) - JObject(typeDiscriminator :: json.obj) - } - - } - - inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): JSON[A] = - new JSON[A] { - private val caseClassMetaData: CaseClassMetaData = AnnotationReader.readCaseClassMetaData[A] - private val jsons: Vector[JSON[Any]] = summonFormatters[mirrorOfProduct.MirroredElemTypes] - private val fieldsAndJsons: Vector[(Field, JSON[Any])] = caseClassMetaData.fields.zip(jsons) - - private val fieldNames: Vector[String] = fieldsAndJsons.flatMap { (field, json) => - if (field.embedded) json.fields.toVector :+ field.name - else Vector(field.name) - } - - override val fields: Set[String] = fieldNames.toSet - - override def write(value: A): JValue = { - val caseClassFields = value.asInstanceOf[Product].productIterator - jsons - .zip(caseClassFields) - .zip(caseClassMetaData.fields) - .foldLeft[JValue](JObject()) { case (jObject, ((json, fieldValue), field)) => - addField(jObject.asInstanceOf[JObject], field, json.write(fieldValue)) - } - } - - override def read(jValue: JValue): JValidation[A] = - jValue match { - case jObject: JObject => - for { - fieldsAsAList <- fieldsAndJsons - .map((field, format) => readField(field, format, jObject)) - .sequence - fieldsAsTuple = Tuple.fromArray(fieldsAsAList.toArray) - - } yield mirrorOfProduct.fromTuple( - fieldsAsTuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) - - case x => - Validated.invalidNel(JSONParseError(s"JSON object expected. $x")) - } - - private def readField(field: Field, json: JSON[Any], jObject: JObject): JValidation[Any] = - if (field.embedded) json.read(jObject) - else io.sphere.json.field(field.fieldName, field.defaultArgument)(jObject)(json) - - } - - inline private def summonFormatters[T <: Tuple]: Vector[JSON[Any]] = - inline erasedValue[T] match { - case _: EmptyTuple => Vector.empty - case _: (t *: ts) => - summonInline[JSON[t]] - .asInstanceOf[JSON[Any]] +: summonFormatters[ts] - } - } -} diff --git a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/DeriveSingleton.scala b/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/DeriveSingleton.scala deleted file mode 100644 index 2b65c923..00000000 --- a/json/json-derivation-scala-3/src/main/scala/io/sphere/json/generic/DeriveSingleton.scala +++ /dev/null @@ -1,82 +0,0 @@ -package io.sphere.json.generic - -import cats.data.Validated -import io.sphere.json.{JSON, JSONParseError, JValidation} -import org.json4s.{JNull, JString, JValue} - -import scala.deriving.Mirror - -inline def deriveSingletonJSON[A](using Mirror.Of[A]): JSON[A] = DeriveSingleton.derived - -object DeriveSingleton { - - inline given derived[A](using Mirror.Of[A]): JSON[A] = Derivation.derived[A] - - private object Derivation { - - import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} - - inline def derived[A](using m: Mirror.Of[A]): JSON[A] = - inline m match { - case s: Mirror.SumOf[A] => deriveTrait(s) - case p: Mirror.ProductOf[A] => deriveObject(p) - } - - inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): JSON[A] = - new JSON[A] { - private val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] - private val typeHintMap: Map[String, String] = traitMetaData.subtypes.collect { - case (name, classMeta) if classMeta.typeHint.isDefined => - name -> classMeta.typeHint.get - } - private val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) - private val jsons: Seq[JSON[Any]] = summonFormatters[mirrorOfSum.MirroredElemTypes] - private val names: Seq[String] = - constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector - .asInstanceOf[Vector[String]] - private val jsonsByNames: Map[String, JSON[Any]] = names.zip(jsons).toMap - - override def read(jValue: JValue): JValidation[A] = - jValue match { - case JString(typeName) => - val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - jsonsByNames.get(originalTypeName) match { - case Some(json) => - json.read(JNull).map(_.asInstanceOf[A]) - case None => - Validated.invalidNel(JSONParseError(s"'$typeName' is not a valid value")) - } - - case x => - Validated.invalidNel(JSONParseError(s"JSON string expected. Got >>> $jValue")) - } - - override def write(value: A): JValue = { - val originalTypeName = value.asInstanceOf[Product].productPrefix - val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) - JString(typeName) - } - - } - - inline private def deriveObject[A](mirrorOfProduct: Mirror.ProductOf[A]): JSON[A] = - new JSON[A] { - override def write(value: A): JValue = ??? // This is already taken care of in `deriveTrait` - - override def read(jValue: JValue): JValidation[A] = { - // Just create the object instance, no need to do anything else - val tuple = Tuple.fromArray(Array.empty[Any]) - val obj = mirrorOfProduct.fromTuple(tuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) - Validated.Valid(obj) - } - } - - inline private def summonFormatters[T <: Tuple]: Vector[JSON[Any]] = - inline erasedValue[T] match { - case _: EmptyTuple => Vector.empty - case _: (t *: ts) => - summonInline[JSON[t]] - .asInstanceOf[JSON[Any]] +: summonFormatters[ts] - } - } -} diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala deleted file mode 100644 index dc334403..00000000 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala +++ /dev/null @@ -1,171 +0,0 @@ -package io.sphere.json - -import cats.data.Validated.Valid -import io.sphere.json.generic.* -import org.json4s.DefaultJsonFormats.given -import org.json4s.{DynamicJValueImplicits, JArray, JObject, JValue} -import org.json4s.JsonAST.{JField, JNothing} -import org.json4s.jackson.JsonMethods.{compact, render} -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class DeriveSingletonJSONSpec extends AnyWordSpec with Matchers { - "DeriveSingletonJSON" must { - "read normal singleton values" in { - val user = getFromJSON[UserWithPicture](""" - { - "userId": "foo-123", - "pictureSize": "Medium", - "pictureUrl": "http://exmple.com" - } - """) - - user must be(UserWithPicture("foo-123", Medium, "http://exmple.com")) - } - - "fail to read if singleton value is unknown" in { - a[JSONException] must be thrownBy getFromJSON[UserWithPicture](""" - { - "userId": "foo-123", - "pictureSize": "foo", - "pictureUrl": "http://exmple.com" - } - """) - } - - "write normal singleton values" in { - val userJson = toJValue(UserWithPicture("foo-123", Medium, "http://exmple.com")) - - val Valid(expectedJson) = parseJSON(""" - { - "userId": "foo-123", - "pictureSize": "Medium", - "pictureUrl": "http://exmple.com" - } - """): @unchecked - - filter(userJson) must be(expectedJson) - } - - "read custom singleton values" in { - val user = getFromJSON[UserWithPicture](""" - { - "userId": "foo-123", - "pictureSize": "bar", - "pictureUrl": "http://exmple.com" - } - """) - - user must be(UserWithPicture("foo-123", Custom, "http://exmple.com")) - } - - "write custom singleton values" in { - val userJson = toJValue(UserWithPicture("foo-123", Custom, "http://exmple.com")) - - val Valid(expectedJson) = parseJSON(""" - { - "userId": "foo-123", - "pictureSize": "bar", - "pictureUrl": "http://exmple.com" - } - """): @unchecked - - filter(userJson) must be(expectedJson) - } - - "write and consequently read, which must produce the original value" in { - val originalUser = UserWithPicture("foo-123", Medium, "http://exmple.com") - val newUser = getFromJSON[UserWithPicture](compact(render(toJValue(originalUser)))) - - newUser must be(originalUser) - } - - "read and write sealed trait with only one subtype" in { - val json = """ - { - "userId": "foo-123", - "pictureSize": "Medium", - "pictureUrl": "http://example.com", - "access": { - "type": "Authorized", - "project": "internal" - } - } - """ - val user = getFromJSON[UserWithPicture](json) - - user must be( - UserWithPicture( - "foo-123", - Medium, - "http://example.com", - Some(Access.Authorized("internal")))) - - val newJson = toJValue[UserWithPicture](user) - Valid(newJson) must be(parseJSON(json)) - - val Valid(newUser) = fromJValue[UserWithPicture](newJson): @unchecked - newUser must be(user) - } - } - - private def filter(jvalue: JValue): JValue = - jvalue.removeField { - case (_, JNothing) => true - case _ => false - } - - extension (jv: JValue) { - def removeField(p: JField => Boolean): JValue = jv.transform { case JObject(l) => - JObject(l.filterNot(p)) - } - - def transform(f: PartialFunction[JValue, JValue]): JValue = map { x => - f.applyOrElse[JValue, JValue](x, _ => x) - } - - def map(f: JValue => JValue): JValue = { - def rec(v: JValue): JValue = v match { - case JObject(l) => f(JObject(l.map { case (n, va) => (n, rec(va)) })) - case JArray(l) => f(JArray(l.map(rec))) - case x => f(x) - } - - rec(jv) - } - } -} - -sealed abstract class PictureSize(val weight: Int, val height: Int) - -case object Small extends PictureSize(100, 100) -case object Medium extends PictureSize(500, 450) -case object Big extends PictureSize(1024, 2048) - -@JSONTypeHint("bar") -case object Custom extends PictureSize(1, 2) - -object PictureSize { - import DeriveSingleton.derived - - given JSON[PictureSize] = deriveSingletonJSON -} - -sealed trait Access -object Access { - // only one sub-type - import JSON.derived - case class Authorized(project: String) extends Access - - given JSON[Access] = deriveJSON -} - -case class UserWithPicture( - userId: String, - pictureSize: PictureSize, - pictureUrl: String, - access: Option[Access] = None) - -object UserWithPicture { - given JSON[UserWithPicture] = deriveJSON[UserWithPicture] -} diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala deleted file mode 100644 index f5c7eba4..00000000 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala +++ /dev/null @@ -1,131 +0,0 @@ -package io.sphere.json - -import org.scalatest.OptionValues -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec -import io.sphere.json.generic._ - -object JSONEmbeddedSpec { - - case class Embedded(value1: String, value2: Int) - - object Embedded { - given JSON[Embedded] = deriveJSON[Embedded] - } - - case class Test1(name: String, @JSONEmbedded embedded: Embedded) - - object Test1 { - given JSON[Test1] = deriveJSON[Test1] - } - - case class Test2(name: String, @JSONEmbedded embedded: Option[Embedded] = None) - - object Test2 { - given JSON[Test2] = deriveJSON - } - - case class SubTest4(@JSONEmbedded embedded: Embedded) - object SubTest4 { - given JSON[SubTest4] = deriveJSON - } - - case class Test4(subField: Option[SubTest4] = None) - object Test4 { - given JSON[Test4] = deriveJSON - } -} - -class JSONEmbeddedSpec extends AnyWordSpec with Matchers with OptionValues { - import JSONEmbeddedSpec._ - - "JSONEmbedded" should { - "flatten the json in one object" in { - val json = - """{ - | "name": "ze name", - | "value1": "ze value1", - | "value2": 45 - |} - """.stripMargin - val test1 = getFromJSON[Test1](json) - test1.name mustEqual "ze name" - test1.embedded.value1 mustEqual "ze value1" - test1.embedded.value2 mustEqual 45 - - val result = toJSON(test1) - parseJSON(result) mustEqual parseJSON(json) - } - - "validate that the json contains all needed fields" in { - val json = - """{ - | "name": "ze name", - | "value1": "ze value1" - |} - """.stripMargin - fromJSON[Test1](json).isInvalid must be(true) - fromJSON[Test1]("""{"name": "a"}""").isInvalid must be(true) - } - - "support optional embedded attribute" in { - val json = - """{ - | "name": "ze name", - | "value1": "ze value1", - | "value2": 45 - |} - """.stripMargin - val test2 = getFromJSON[Test2](json) - test2.name mustEqual "ze name" - test2.embedded.value.value1 mustEqual "ze value1" - test2.embedded.value.value2 mustEqual 45 - - val result = toJSON(test2) - parseJSON(result) mustEqual parseJSON(json) - } - - "ignore unknown fields" in { - val json = - """{ - | "name": "ze name", - | "value1": "ze value1", - | "value2": 45, - | "value3": true - |} - """.stripMargin - val test2 = getFromJSON[Test2](json) - test2.name mustEqual "ze name" - test2.embedded.value.value1 mustEqual "ze value1" - test2.embedded.value.value2 mustEqual 45 - } - - "check for sub-fields" in { - val json = - """ - { - "subField": { - "value1": "ze value1", - "value2": 45 - } - } - """ - val test4 = getFromJSON[Test4](json) - test4.subField.value.embedded.value1 mustEqual "ze value1" - test4.subField.value.embedded.value2 mustEqual 45 - } - - "support the absence of optional embedded attribute" in { - val json = """{ "name": "ze name" }""" - val test2 = getFromJSON[Test2](json) - test2.name mustEqual "ze name" - test2.embedded mustEqual None - } - - "validate the absence of some embedded attributes" in { - val json = """{ "name": "ze name", "value1": "ze value1" }""" - fromJSON[Test2](json).isInvalid must be(true) - } - } - -} diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala deleted file mode 100644 index 240ac465..00000000 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/JSONSpec.scala +++ /dev/null @@ -1,391 +0,0 @@ -package io.sphere.json - -import cats.data.Validated.{Invalid, Valid} -import cats.data.ValidatedNel -import cats.syntax.apply.* -import org.json4s.JsonAST.* -import io.sphere.json.field -import io.sphere.json.generic.* -import io.sphere.util.Money -import org.joda.time.* -import org.scalatest.matchers.must.Matchers -import org.scalatest.funspec.AnyFunSpec -import org.json4s.DefaultJsonFormats.given - -object JSONSpec { - case class Project(nr: Int, name: String, version: Int = 1, milestones: List[Milestone] = Nil) - case class Milestone(name: String, date: Option[DateTime] = None) - - sealed abstract class Animal - case class Dog(name: String) extends Animal - case class Cat(name: String) extends Animal - case class Bird(name: String) extends Animal - - sealed trait GenericBase[A] - case class GenericA[A](a: A) extends GenericBase[A] - case class GenericB[A](a: A) extends GenericBase[A] - - object Singleton - - sealed abstract class SingletonEnum - case object SingletonA extends SingletonEnum - case object SingletonB extends SingletonEnum - case object SingletonC extends SingletonEnum - - sealed trait Mixed - case object SingletonMixed extends Mixed - case class RecordMixed(i: Int) extends Mixed - - object ScalaEnum extends Enumeration { - val One, Two, Three = Value - } - - case class Node( - value: Option[List[Node]]) // JSON instances for recursive data types cannot be derived -} - -class JSONSpec extends AnyFunSpec with Matchers { - import JSONSpec._ - - describe("JSON") { - it("must read/write a custom class using custom typeclass instances") { - import JSONSpec.{Milestone, Project} - - implicit object MilestoneJSON extends JSON[Milestone] { - def write(m: Milestone): JValue = JObject( - JField("name", JString(m.name)) :: - JField("date", toJValue(m.date)) :: Nil - ) - def read(j: JValue): ValidatedNel[JSONError, Milestone] = j match { - case o: JObject => - (field[String]("name")(o), field[Option[DateTime]]("date")(o)).mapN(Milestone.apply) - case _ => fail("JSON object expected.") - } - } - implicit object ProjectJSON extends JSON[Project] { - def write(p: Project): JValue = JObject( - JField("nr", JInt(p.nr)) :: - JField("name", JString(p.name)) :: - JField("version", JInt(p.version)) :: - JField("milestones", toJValue(p.milestones)) :: - Nil - ) - def read(jval: JValue): ValidatedNel[JSONError, Project] = jval match { - case o: JObject => - ( - field[Int]("nr")(o), - field[String]("name")(o), - field[Int]("version", Some(1))(o), - field[List[Milestone]]("milestones", Some(Nil))(o)).mapN(Project.apply) - case _ => fail("JSON object expected.") - } - } - - val proj = Project(42, "Linux") - fromJSON[Project](toJSON(proj)) must equal(Valid(proj)) - - // Now some invalid JSON to test the error accumulation - val wrongTypeJSON = """ - { - "nr":"1", - "name":23, - "version":1, - "milestones":[{"name":"Bravo", "date": "xxx"}] - } - """ - - val Invalid(errs) = fromJSON[Project](wrongTypeJSON): @unchecked - errs.toList must equal( - List( - JSONFieldError(List("nr"), "JSON Number in the range of an Int expected."), - JSONFieldError(List("name"), "JSON String expected."), - JSONFieldError(List("milestones", "date"), "Failed to parse date/time: xxx") - )) - - // Now without a version value and without a milestones list. Defaults should apply. - val noVersionJSON = """{"nr":1,"name":"Linux"}""" - fromJSON[Project](noVersionJSON) must equal(Valid(Project(1, "Linux"))) - } - - it("must fail reading wrong currency code.") { - val wrongMoney = """{"currencyCode":"WRONG","centAmount":1000}""" - fromJSON[Money](wrongMoney).isInvalid must be(true) - } - - it("must provide derived JSON instances for product types (case classes)") { - import JSONSpec.{Milestone, Project} - given JSON[Milestone] = deriveJSON[Milestone] - given JSON[Project] = deriveJSON[Project] - val proj = - Project(42, "Linux", 7, Milestone("1.0") :: Milestone("2.0") :: Milestone("3.0") :: Nil) - fromJSON[Project](toJSON(proj)) must equal(Valid(proj)) - } - - it("must handle empty String") { - val Invalid(err) = fromJSON[Int](""): @unchecked - err.toList.head mustBe a[JSONParseError] - } - - it("must provide user-friendly error by empty String") { - val Invalid(err) = fromJSON[Int](""): @unchecked - err.toList mustEqual List(JSONParseError("No content to map due to end-of-input")) - } - - it("must handle incorrect json") { - val Invalid(err) = fromJSON[Int]("""{"key: "value"}"""): @unchecked - err.toList.head mustBe a[JSONParseError] - } - - it("must provide user-friendly error by incorrect json") { - val Invalid(err) = fromJSON[Int]("""{"key: "value"}"""): @unchecked - err.toList mustEqual List(JSONParseError( - "Unexpected character ('v' (code 118)): was expecting a colon to separate field name and value")) - } - - it("must provide derived JSON instances for sum types") { - import io.sphere.json.generic.JSON.derived - given JSON[Animal] = deriveJSON - List(Bird("Peewee"), Dog("Hasso"), Cat("Felidae")).foreach { animal => - fromJSON[Animal](toJSON(animal)) must equal(Valid(animal)) - } - } - - it("must provide derived instances for product types with concrete type parameters") { - given JSON[GenericA[String]] = deriveJSON[GenericA[String]] - val a = GenericA("hello") - fromJSON[GenericA[String]](toJSON(a)) must equal(Valid(a)) - } - - it("must provide derived instances for product types with generic type parameters") { - implicit def aJSON[A: JSON]: JSON[GenericA[A]] = deriveJSON[GenericA[A]] - val a = GenericA("hello") - fromJSON[GenericA[String]](toJSON(a)) must equal(Valid(a)) - } - -// it("must provide derived instances for singleton objects") { -// import io.sphere.json.generic.JSON.derived -// implicit val singletonJSON: JSON[JSONSpec.Singleton.type] = deriveJSON[JSONSpec.Singleton.type] -// -// val json = s"""[${toJSON(Singleton)}]""" -// withClue(json) { -// fromJSON[Seq[Singleton.type]](json) must equal(Valid(Seq(Singleton))) -// } -// -// implicit val singleEnumJSON: JSON[SingletonEnum] = deriveJSON[SingletonEnum] -// List(SingletonA, SingletonB, SingletonC).foreach { s => -// fromJSON[SingletonEnum](toJSON(s)) must equal(Valid(s)) -// } -// } - - it("must provide derived instances for sum types with a mix of case class / object") { - import io.sphere.json.generic.JSON.derived - given JSON[Mixed] = deriveJSON - List(SingletonMixed, RecordMixed(1)).foreach { m => - fromJSON[Mixed](toJSON(m)) must equal(Valid(m)) - } - } -// -// it("must provide derived instances for scala.Enumeration") { -// import io.sphere.json.generic.JSON.derived -// implicit val scalaEnumJSON: JSON[JSONSpec.ScalaEnum.Value] = deriveJSON[ScalaEnum.Value] -// ScalaEnum.values.foreach { v => -// val json = s"""[${toJSON(v)}]""" -// withClue(json) { -// fromJSON[Seq[ScalaEnum.Value]](json) must equal(Valid(Seq(v))) -// } -// } -// } -// -// it("must handle subclasses correctly in `jsonTypeSwitch`") { -// implicit val jsonImpl = TestSubjectBase.json -// -// val testSubjects = List[TestSubjectBase]( -// TestSubjectConcrete1("testSubject1"), -// TestSubjectConcrete2("testSubject2"), -// TestSubjectConcrete3("testSubject3"), -// TestSubjectConcrete4("testSubject4") -// ) -// -// testSubjects.foreach { testSubject => -// val json = toJSON(testSubject) -// withClue(json) { -// fromJSON[TestSubjectBase](json) must equal(Valid(testSubject)) -// } -// } -// -// } -// -// } -// -// describe("ToJSON and FromJSON") { -// it("must provide derived JSON instances for sum types") { -// // ToJSON -// implicit val birdToJSON = deriveJSON[Bird].write(Bird.apply _) -// implicit val dogToJSON = deriveJSON[Dog].write(Dog.apply _) -// implicit val catToJSON = toJsonProduct(Cat.apply _) -// implicit val animalToJSON = toJsonTypeSwitch[Animal, Bird, Dog, Cat](Nil) -// // FromJSON -// implicit val birdFromJSON = fromJsonProduct(Bird.apply _) -// implicit val dogFromJSON = fromJsonProduct(Dog.apply _) -// implicit val catFromJSON = fromJsonProduct(Cat.apply _) -// implicit val animalFromJSON = fromJsonTypeSwitch[Animal, Bird, Dog, Cat](Nil) -// -// List(Bird("Peewee"), Dog("Hasso"), Cat("Felidae")).foreach { -// a: Animal => -// fromJSON[Animal](toJSON(a)) must equal(Valid(a)) -// } -// } -// -// it("must provide derived instances for product types with concrete type parameters") { -// implicit val aToJSON = toJsonProduct(GenericA.apply[String] _) -// implicit val aFromJSON = fromJsonProduct(GenericA.apply[String] _) -// val a = GenericA("hello") -// fromJSON[GenericA[String]](toJSON(a)) must equal(Valid(a)) -// } -// -// it("must provide derived instances for singleton objects") { -// implicit val toSingletonJSON = toJsonSingleton(Singleton) -// implicit val fromSingletonJSON = fromJsonSingleton(Singleton) -// val json = s"""[${toJSON(Singleton)}]""" -// withClue(json) { -// fromJSON[Seq[Singleton.type]](json) must equal(Valid(Seq(Singleton))) -// } -// -// // ToJSON -// implicit val toSingleAJSON = toJsonSingleton(SingletonA) -// implicit val toSingleBJSON = toJsonSingleton(SingletonB) -// implicit val toSingleCJSON = toJsonSingleton(SingletonC) -// implicit val toSingleEnumJSON = -// toJsonSingletonEnumSwitch[SingletonEnum, SingletonA.type, SingletonB.type, SingletonC.type]( -// Nil) -// // FromJSON -// implicit val fromSingleAJSON = fromJsonSingleton(SingletonA) -// implicit val fromSingleBJSON = fromJsonSingleton(SingletonB) -// implicit val fromSingleCJSON = fromJsonSingleton(SingletonC) -// implicit val fromSingleEnumJSON = fromJsonSingletonEnumSwitch[ -// SingletonEnum, -// SingletonA.type, -// SingletonB.type, -// SingletonC.type](Nil) -// -// List(SingletonA, SingletonB, SingletonC).foreach { -// s: SingletonEnum => -// fromJSON[SingletonEnum](toJSON(s)) must equal(Valid(s)) -// } -// } -// -// it("must provide derived instances for sum types with a mix of case class / object") { -// // ToJSON -// implicit val toSingleJSON = toJsonProduct0(SingletonMixed) -// implicit val toRecordJSON = toJsonProduct(RecordMixed.apply _) -// implicit val toMixedJSON = toJsonTypeSwitch[Mixed, SingletonMixed.type, RecordMixed](Nil) -// // FromJSON -// implicit val fromSingleJSON = fromJsonProduct0(SingletonMixed) -// implicit val fromRecordJSON = fromJsonProduct(RecordMixed.apply _) -// implicit val fromMixedJSON = fromJsonTypeSwitch[Mixed, SingletonMixed.type, RecordMixed](Nil) -// List(SingletonMixed, RecordMixed(1)).foreach { -// m: Mixed => -// fromJSON[Mixed](toJSON(m)) must equal(Valid(m)) -// } -// } -// -// it("must provide derived instances for scala.Enumeration") { -// implicit val toScalaEnumJSON = toJsonEnum(ScalaEnum) -// implicit val fromScalaEnumJSON = fromJsonEnum(ScalaEnum) -// ScalaEnum.values.foreach { v => -// val json = s"""[${toJSON(v)}]""" -// withClue(json) { -// fromJSON[Seq[ScalaEnum.Value]](json) must equal(Valid(Seq(v))) -// } -// } -// } -// -// it("must handle subclasses correctly in `jsonTypeSwitch`") { -// // ToJSON -// implicit val to1 = toJsonProduct(TestSubjectConcrete1.apply _) -// implicit val to2 = toJsonProduct(TestSubjectConcrete2.apply _) -// implicit val to3 = toJsonProduct(TestSubjectConcrete3.apply _) -// implicit val to4 = toJsonProduct(TestSubjectConcrete4.apply _) -// implicit val toA = -// toJsonTypeSwitch[TestSubjectCategoryA, TestSubjectConcrete1, TestSubjectConcrete2](Nil) -// implicit val toB = -// toJsonTypeSwitch[TestSubjectCategoryB, TestSubjectConcrete3, TestSubjectConcrete4](Nil) -// implicit val toBase = -// toJsonTypeSwitch[TestSubjectBase, TestSubjectCategoryA, TestSubjectCategoryB](Nil) -// -// // FromJSON -// implicit val from1 = fromJsonProduct(TestSubjectConcrete1.apply _) -// implicit val from2 = fromJsonProduct(TestSubjectConcrete2.apply _) -// implicit val from3 = fromJsonProduct(TestSubjectConcrete3.apply _) -// implicit val from4 = fromJsonProduct(TestSubjectConcrete4.apply _) -// implicit val fromA = -// fromJsonTypeSwitch[TestSubjectCategoryA, TestSubjectConcrete1, TestSubjectConcrete2](Nil) -// implicit val fromB = -// fromJsonTypeSwitch[TestSubjectCategoryB, TestSubjectConcrete3, TestSubjectConcrete4](Nil) -// implicit val fromBase = -// fromJsonTypeSwitch[TestSubjectBase, TestSubjectCategoryA, TestSubjectCategoryB](Nil) -// -// val testSubjects = List[TestSubjectBase]( -// TestSubjectConcrete1("testSubject1"), -// TestSubjectConcrete2("testSubject2"), -// TestSubjectConcrete3("testSubject3"), -// TestSubjectConcrete4("testSubject4") -// ) -// -// testSubjects.foreach { testSubject => -// val json = toJSON(testSubject) -// withClue(json) { -// fromJSON[TestSubjectBase](json) must equal(Valid(testSubject)) -// } -// } -// -// } -// -// it("must provide derived JSON instances for product types (case classes)") { -// import JSONSpec.{Milestone, Project} -// // ToJSON -// implicit val milestoneToJSON = toJsonProduct(Milestone.apply _) -// implicit val projectToJSON = toJsonProduct(Project.apply _) -// // FromJSON -// implicit val milestoneFromJSON = fromJsonProduct(Milestone.apply _) -// implicit val projectFromJSON = fromJsonProduct(Project.apply _) -// -// val proj = -// Project(42, "Linux", 7, Milestone("1.0") :: Milestone("2.0") :: Milestone("3.0") :: Nil) -// fromJSON[Project](toJSON(proj)) must equal(Valid(proj)) -// } - } -} - -abstract class TestSubjectBase - -sealed abstract class TestSubjectCategoryA extends TestSubjectBase -sealed abstract class TestSubjectCategoryB extends TestSubjectBase - -@JSONTypeHint("foo") -case class TestSubjectConcrete1(c1: String) extends TestSubjectCategoryA -case class TestSubjectConcrete2(c2: String) extends TestSubjectCategoryA - -case class TestSubjectConcrete3(c3: String) extends TestSubjectCategoryB -case class TestSubjectConcrete4(c4: String) extends TestSubjectCategoryB - -object TestSubjectCategoryA { - - import io.sphere.json.generic.JSON.derived - val json: JSON[TestSubjectCategoryA] = deriveJSON[TestSubjectCategoryA] -} - -object TestSubjectCategoryB { - - import io.sphere.json.generic.JSON.derived - val json: JSON[TestSubjectCategoryB] = deriveJSON[TestSubjectCategoryB] -} - -//object TestSubjectBase { -// val json: JSON[TestSubjectBase] = { -// implicit val jsonA = TestSubjectCategoryA.json -// implicit val jsonB = TestSubjectCategoryB.json -// -// jsonTypeSwitch[TestSubjectBase, TestSubjectCategoryA, TestSubjectCategoryB](Nil) -// } -//} diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/NullHandlingSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/NullHandlingSpec.scala deleted file mode 100644 index 5450d9e2..00000000 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/NullHandlingSpec.scala +++ /dev/null @@ -1,68 +0,0 @@ -package io.sphere.json - -import io.sphere.json.generic._ -import org.json4s.JsonAST.{JNothing, JObject} -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class NullHandlingSpec extends AnyWordSpec with Matchers { - "JSON deserialization" must { - "accept undefined fields and use default values for them" in { - val jeans = getFromJSON[Jeans]("{}") - - jeans must be(Jeans(None, None, Set.empty, "secret")) - } - - "accept null values and use default values for them" in { - val jeans = getFromJSON[Jeans](""" - { - "leftPocket": null, - "rightPocket": null, - "backPocket": null, - "hiddenPocket": null - } - """) - - jeans must be(Jeans(None, None, Set.empty, "secret")) - } - - "accept JNothing values and use default values for them" in { - val jeans = getFromJValue[Jeans]( - JObject( - "leftPocket" -> JNothing, - "rightPocket" -> JNothing, - "backPocket" -> JNothing, - "hiddenPocket" -> JNothing)) - - jeans must be(Jeans(None, None, Set.empty, "secret")) - } - - "accept not-null values and use them" in { - val jeans = getFromJSON[Jeans](""" - { - "leftPocket": "Axe", - "rightPocket": "Magic powder", - "backPocket": ["Magic wand", "Rusty sword"], - "hiddenPocket": "The potion of healing" - } - """) - - jeans must be( - Jeans( - Some("Axe"), - Some("Magic powder"), - Set("Magic wand", "Rusty sword"), - "The potion of healing")) - } - } -} - -case class Jeans( - leftPocket: Option[String] = None, - rightPocket: Option[String], - backPocket: Set[String] = Set.empty, - hiddenPocket: String = "secret") - -object Jeans { - given JSON[Jeans] = deriveJSON[Jeans] -} diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/OptionReaderSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/OptionReaderSpec.scala deleted file mode 100644 index 8461d962..00000000 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/OptionReaderSpec.scala +++ /dev/null @@ -1,150 +0,0 @@ -package io.sphere.json - -import io.sphere.json.generic._ -import org.json4s.{JArray, JLong, JNothing, JObject, JString} -import org.scalatest.OptionValues -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec -import org.json4s.DefaultJsonFormats.given - -object OptionReaderSpec { - - case class SimpleClass(value1: String, value2: Int) - - object SimpleClass { - given JSON[SimpleClass] = deriveJSON[SimpleClass] - } - - case class ComplexClass(name: String, simpleClass: Option[SimpleClass]) - - object ComplexClass { - given JSON[ComplexClass] = deriveJSON[ComplexClass] - } - - case class MapClass(id: Long, map: Option[Map[String, String]]) - object MapClass { - given JSON[MapClass] = deriveJSON[MapClass] - } - - case class ListClass(id: Long, list: Option[List[String]]) - object ListClass { - given JSON[ListClass] = deriveJSON[ListClass] - } -} - -class OptionReaderSpec extends AnyWordSpec with Matchers with OptionValues { - import OptionReaderSpec._ - - "OptionReader" should { - "handle presence of all fields" in { - val json = - """{ - | "value1": "a", - | "value2": 45 - |} - """.stripMargin - val result = getFromJSON[Option[SimpleClass]](json) - result.value.value1 mustEqual "a" - result.value.value2 mustEqual 45 - } - - "handle presence of all fields mixed with ignored fields" in { - val json = - """{ - | "value1": "a", - | "value2": 45, - | "value3": "b" - |} - """.stripMargin - val result = getFromJSON[Option[SimpleClass]](json) - result.value.value1 mustEqual "a" - result.value.value2 mustEqual 45 - } - - "handle presence of not all the fields" in { - val json = """{ "value1": "a" }""" - fromJSON[Option[SimpleClass]](json).isInvalid must be(true) - } - - "handle absence of all fields" in { - val json = "{}" - val result = getFromJSON[Option[SimpleClass]](json) - result must be(None) - } - - "handle optional map" in { - getFromJValue[MapClass](JObject("id" -> JLong(1L))) mustEqual MapClass(1L, None) - - getFromJValue[MapClass](JObject("id" -> JLong(1L), "map" -> JObject())) mustEqual - MapClass(1L, Some(Map.empty)) - - getFromJValue[MapClass]( - JObject("id" -> JLong(1L), "map" -> JObject("a" -> JString("b")))) mustEqual - MapClass(1L, Some(Map("a" -> "b"))) - - toJValue[MapClass](MapClass(1L, None)) mustEqual - JObject("id" -> JLong(1L), "map" -> JNothing) - toJValue[MapClass](MapClass(1L, Some(Map()))) mustEqual - JObject("id" -> JLong(1L), "map" -> JObject()) - toJValue[MapClass](MapClass(1L, Some(Map("a" -> "b")))) mustEqual - JObject("id" -> JLong(1L), "map" -> JObject("a" -> JString("b"))) - } - - "handle optional list" in { - getFromJValue[ListClass]( - JObject("id" -> JLong(1L), "list" -> JArray(List(JString("hi"))))) mustEqual - ListClass(1L, Some(List("hi"))) - getFromJValue[ListClass](JObject("id" -> JLong(1L), "list" -> JArray(List.empty))) mustEqual - ListClass(1L, Some(List())) - getFromJValue[ListClass](JObject("id" -> JLong(1L))) mustEqual - ListClass(1L, None) - - toJValue(ListClass(1L, Some(List("hi")))) mustEqual JObject( - "id" -> JLong(1L), - "list" -> JArray(List(JString("hi")))) - toJValue(ListClass(1L, Some(List.empty))) mustEqual JObject( - "id" -> JLong(1L), - "list" -> JArray(List.empty)) - toJValue(ListClass(1L, None)) mustEqual JObject("id" -> JLong(1L), "list" -> JNothing) - } - - "handle absence of all fields mixed with ignored fields" in { - val json = """{ "value3": "a" }""" - val result = getFromJSON[Option[SimpleClass]](json) - result must be(None) - } - - "consider all fields if the data type does not impose any restriction" in { - val json = - """{ - | "key1": "value1", - | "key2": "value2" - |} - """.stripMargin - val expected = Map("key1" -> "value1", "key2" -> "value2") - val result = getFromJSON[Map[String, String]](json) - result mustEqual expected - - val maybeResult = getFromJSON[Option[Map[String, String]]](json) - maybeResult.value mustEqual expected - } - - "parse optional element" in { - val json = - """{ - | "name": "ze name", - | "simpleClass": { - | "value1": "value1", - | "value2": 42 - | } - |} - """.stripMargin - val result = getFromJSON[ComplexClass](json) - result.simpleClass.value.value1 mustEqual "value1" - result.simpleClass.value.value2 mustEqual 42 - - parseJSON(toJSON(result)) mustEqual parseJSON(json) - } - } - -} diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/TypesSwitchSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/TypesSwitchSpec.scala deleted file mode 100644 index 88f49334..00000000 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/TypesSwitchSpec.scala +++ /dev/null @@ -1,87 +0,0 @@ -//package io.sphere.json -// -//import io.sphere.json.generic.{TypeSelectorContainer, deriveJSON, jsonTypeSwitch} -//import org.json4s._ -//import org.scalatest.matchers.must.Matchers -//import org.scalatest.wordspec.AnyWordSpec -// -//class TypesSwitchSpec extends AnyWordSpec with Matchers { -// import TypesSwitchSpec._ -// -// "jsonTypeSwitch" must { -// "combine different sum types tree" in { -// val m: Seq[Message] = List( -// TypeA.ClassA1(23), -// TypeA.ClassA2("world"), -// TypeB.ClassB1(valid = false), -// TypeB.ClassB2(Seq("a23", "c62"))) -// -// val jsons = m.map(Message.json.write) -// jsons must be( -// List( -// JObject("number" -> JLong(23), "type" -> JString("ClassA1")), -// JObject("name" -> JString("world"), "type" -> JString("ClassA2")), -// JObject("valid" -> JBool(false), "type" -> JString("ClassB1")), -// JObject( -// "references" -> JArray(List(JString("a23"), JString("c62"))), -// "type" -> JString("ClassB2")) -// )) -// -// val messages = jsons.map(Message.json.read).map(_.toOption.get) -// messages must be(m) -// } -// } -// -// "TypeSelectorContainer" must { -// "have information about type value discriminators" in { -// val selectors = Message.json.typeSelectors -// selectors.map(_.typeValue) must contain.allOf( -// "ClassA1", -// "ClassA2", -// "TypeA", -// "ClassB1", -// "ClassB2", -// "TypeB") -// -// // I don't think it's useful to allow different type fields. How is it possible to deserialize one json -// // if different type fields are used? -// selectors.map(_.typeField) must be(List("type", "type", "type", "type", "type", "type")) -// -// selectors.map(_.clazz.getName) must contain.allOf( -// "io.sphere.json.TypesSwitchSpec$TypeA$ClassA1", -// "io.sphere.json.TypesSwitchSpec$TypeA$ClassA2", -// "io.sphere.json.TypesSwitchSpec$TypeA", -// "io.sphere.json.TypesSwitchSpec$TypeB$ClassB1", -// "io.sphere.json.TypesSwitchSpec$TypeB$ClassB2", -// "io.sphere.json.TypesSwitchSpec$TypeB" -// ) -// } -// } -// -//} -// -//object TypesSwitchSpec { -// -// trait Message -// object Message { -// // this can be dangerous is the same class name is used in both sum types -// // ex if we define TypeA.Class1 && TypeB.Class1 -// // as both will use the same type value discriminator -// implicit val json: JSON[Message] with TypeSelectorContainer = -// jsonTypeSwitch[Message, TypeA, TypeB](Nil) -// } -// -// sealed trait TypeA extends Message -// object TypeA { -// case class ClassA1(number: Int) extends TypeA -// case class ClassA2(name: String) extends TypeA -// implicit val json: JSON[TypeA] = deriveJSON[TypeA] -// } -// -// sealed trait TypeB extends Message -// object TypeB { -// case class ClassB1(valid: Boolean) extends TypeB -// case class ClassB2(references: Seq[String]) extends TypeB -// implicit val json: JSON[TypeB] = deriveJSON[TypeB] -// } -//} diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala deleted file mode 100644 index 7bca45fe..00000000 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala +++ /dev/null @@ -1,44 +0,0 @@ -package io.sphere.json.generic - -import io.sphere.json._ -import io.sphere.json.generic._ -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class DefaultValuesSpec extends AnyWordSpec with Matchers { - import DefaultValuesSpec._ - - "deriving JSON" must { - "handle default values" in { - val json = "{ }" - val test = getFromJSON[Test](json) - test.value1 must be("hello") - test.value2 must be(None) - test.value3 must be(Some("hi")) - } - "handle Option with no explicit default values" in { - val json = "{ }" - val test2 = getFromJSON[Test2](json) - test2.value1 must be("hello") - test2.value2 must be(None) - } - } -} - -object DefaultValuesSpec { - case class Test( - value1: String = "hello", - value2: Option[String] = None, - value3: Option[String] = Some("hi") - ) - object Test { - given JSON[Test] = deriveJSON[Test] - } - case class Test2( - value1: String = "hello", - value2: Option[String] - ) - object Test2 { - given JSON[Test2] = deriveJSON[Test2] - } -} diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JSONKeySpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JSONKeySpec.scala deleted file mode 100644 index df2bb582..00000000 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JSONKeySpec.scala +++ /dev/null @@ -1,47 +0,0 @@ -package io.sphere.json.generic - -import org.json4s.MonadicJValue.jvalueToMonadic -import org.json4s.jvalue2readerSyntax -import io.sphere.json._ -import io.sphere.json.generic._ -import org.json4s.DefaultReaders._ -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class JSONKeySpec extends AnyWordSpec with Matchers { - import JSONKeySpec._ - - "deriving JSON" must { - "rename fields annotated with @JSONKey" in { - val test = - Test(value1 = "value1", value2 = "value2", subTest = SubTest(value2 = "other_value2")) - - val json = toJValue(test) - (json \ "value1").as[Option[String]] must be(Some("value1")) - (json \ "value2").as[Option[String]] must be(None) - (json \ "new_value_2").as[Option[String]] must be(Some("value2")) - (json \ "new_sub_value_2").as[Option[String]] must be(Some("other_value2")) - - val newTest = getFromJValue[Test](json) - newTest must be(test) - } - } -} - -object JSONKeySpec { - case class SubTest( - @JSONKey("new_sub_value_2") value2: String - ) - object SubTest { - given JSON[SubTest] = deriveJSON[SubTest] - } - - case class Test( - value1: String, - @JSONKey("new_value_2") value2: String, - @JSONEmbedded subTest: SubTest - ) - object Test { - given JSON[Test] = deriveJSON[Test] - } -} diff --git a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala b/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala deleted file mode 100644 index 4658d1d0..00000000 --- a/json/json-derivation-scala-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala +++ /dev/null @@ -1,63 +0,0 @@ -package io.sphere.json.generic - -import cats.data.Validated.Valid -import io.sphere.json.* -import org.json4s.* -import org.scalatest.Inside -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class JsonTypeHintFieldSpec extends AnyWordSpec with Matchers with Inside { - import JsonTypeHintFieldSpec._ - - "JSONTypeHintField" must { - "allow to set another field to distinguish between types (toJValue)" in { - val user = UserWithPicture("foo-123", Medium, "http://example.com") - val expected = JObject( - List( - "userId" -> JString("foo-123"), - "pictureSize" -> JObject(List("pictureType" -> JString("Medium"))), - "pictureUrl" -> JString("http://example.com"))) - - val json = toJValue[UserWithPicture](user) - json must be(expected) - - inside(fromJValue[UserWithPicture](json)) { case Valid(parsedUser) => - parsedUser must be(user) - } - } - - "allow to set another field to distinguish between types (fromJSON)" in { - val json = - """ - { - "userId": "foo-123", - "pictureSize": { "pictureType": "Medium" }, - "pictureUrl": "http://example.com" - } - """ - - val Valid(user) = fromJSON[UserWithPicture](json): @unchecked - - user must be(UserWithPicture("foo-123", Medium, "http://example.com")) - } - } - -} - -object JsonTypeHintFieldSpec { - - @JSONTypeHintField(value = "pictureType") - sealed trait PictureSize - case object Small extends PictureSize - case object Medium extends PictureSize - case object Big extends PictureSize - - case class UserWithPicture(userId: String, pictureSize: PictureSize, pictureUrl: String) - - object UserWithPicture { - import io.sphere.json.generic.JSON.given - import io.sphere.json.generic.deriveJSON - given JSON[UserWithPicture] = deriveJSON[UserWithPicture] - } -} From e78e849b6b9476e39436658ba64b9178d9774e8d Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 11 Apr 2025 14:20:36 +0200 Subject: [PATCH 070/142] 1. Move remaining tests to json-core 2. remove scala-3 directory --- .../io/sphere/json/SphereJsonParser.scala | 19 -- .../sphere/json/catsinstances/package.scala | 41 ----- .../main/scala/io/sphere/json/package.scala | 116 ------------ .../io/sphere/json/BigNumberParsingSpec.scala | 24 --- .../io/sphere/json/DateTimeParsingSpec.scala | 173 ------------------ .../scala/io/sphere/json/JSONProperties.scala | 170 ----------------- .../io/sphere/json/JodaJavaTimeCompat.scala | 68 ------- .../io/sphere/json/MoneyMarshallingSpec.scala | 110 ----------- .../io/sphere/json/SetHandlingSpec.scala | 17 -- .../io/sphere/json/SphereJsonExample.scala | 46 ----- .../io/sphere/json/SphereJsonParserSpec.scala | 14 -- .../scala/io/sphere/json/ToJSONSpec.scala | 34 ---- .../catsinstances/JSONCatsInstancesTest.scala | 53 ------ .../sphere/json/DeriveSingletonJSONSpec.scala | 0 .../io/sphere/json/JSONEmbeddedSpec.scala | 0 .../io/sphere/json/NullHandlingSpec.scala | 0 .../io/sphere/json/OptionReaderSpec.scala | 0 .../json/generic/DefaultValuesSpec.scala | 0 .../io/sphere/json/generic/JSONKeySpec.scala | 0 .../io/sphere/json/generic}/JSONSpec.scala | 5 +- .../json/generic/JsonTypeHintFieldSpec.scala | 0 .../json/generic/JsonTypeSwitchSpec.scala | 0 22 files changed, 3 insertions(+), 887 deletions(-) delete mode 100644 json/json-3/src/main/scala/io/sphere/json/SphereJsonParser.scala delete mode 100644 json/json-3/src/main/scala/io/sphere/json/catsinstances/package.scala delete mode 100644 json/json-3/src/main/scala/io/sphere/json/package.scala delete mode 100644 json/json-3/src/test/scala/io/sphere/json/BigNumberParsingSpec.scala delete mode 100644 json/json-3/src/test/scala/io/sphere/json/DateTimeParsingSpec.scala delete mode 100644 json/json-3/src/test/scala/io/sphere/json/JSONProperties.scala delete mode 100644 json/json-3/src/test/scala/io/sphere/json/JodaJavaTimeCompat.scala delete mode 100644 json/json-3/src/test/scala/io/sphere/json/MoneyMarshallingSpec.scala delete mode 100644 json/json-3/src/test/scala/io/sphere/json/SetHandlingSpec.scala delete mode 100644 json/json-3/src/test/scala/io/sphere/json/SphereJsonExample.scala delete mode 100644 json/json-3/src/test/scala/io/sphere/json/SphereJsonParserSpec.scala delete mode 100644 json/json-3/src/test/scala/io/sphere/json/ToJSONSpec.scala delete mode 100644 json/json-3/src/test/scala/io/sphere/json/catsinstances/JSONCatsInstancesTest.scala rename json/{json-3/src/test/scala => json-core/src/test/scala-3}/io/sphere/json/DeriveSingletonJSONSpec.scala (100%) rename json/{json-3/src/test/scala => json-core/src/test/scala-3}/io/sphere/json/JSONEmbeddedSpec.scala (100%) rename json/{json-3/src/test/scala => json-core/src/test/scala-3}/io/sphere/json/NullHandlingSpec.scala (100%) rename json/{json-3/src/test/scala => json-core/src/test/scala-3}/io/sphere/json/OptionReaderSpec.scala (100%) rename json/{json-3/src/test/scala => json-core/src/test/scala-3}/io/sphere/json/generic/DefaultValuesSpec.scala (100%) rename json/{json-3/src/test/scala => json-core/src/test/scala-3}/io/sphere/json/generic/JSONKeySpec.scala (100%) rename json/{json-3/src/test/scala/io/sphere/json => json-core/src/test/scala-3/io/sphere/json/generic}/JSONSpec.scala (99%) rename json/{json-3/src/test/scala => json-core/src/test/scala-3}/io/sphere/json/generic/JsonTypeHintFieldSpec.scala (100%) rename json/{json-3/src/test/scala => json-core/src/test/scala-3}/io/sphere/json/generic/JsonTypeSwitchSpec.scala (100%) diff --git a/json/json-3/src/main/scala/io/sphere/json/SphereJsonParser.scala b/json/json-3/src/main/scala/io/sphere/json/SphereJsonParser.scala deleted file mode 100644 index 12714da6..00000000 --- a/json/json-3/src/main/scala/io/sphere/json/SphereJsonParser.scala +++ /dev/null @@ -1,19 +0,0 @@ -package io.sphere.json - -import com.fasterxml.jackson.databind.DeserializationFeature.{ - USE_BIG_DECIMAL_FOR_FLOATS, - USE_BIG_INTEGER_FOR_INTS -} -import com.fasterxml.jackson.databind.ObjectMapper -import org.json4s.jackson.{Json4sScalaModule, JsonMethods} - -// extends the default JsonMethods to configure a different default jackson parser -private object SphereJsonParser extends JsonMethods { - override val mapper: ObjectMapper = { - val m = new ObjectMapper() - m.registerModule(new Json4sScalaModule) - m.configure(USE_BIG_INTEGER_FOR_INTS, false) - m.configure(USE_BIG_DECIMAL_FOR_FLOATS, false) - m - } -} diff --git a/json/json-3/src/main/scala/io/sphere/json/catsinstances/package.scala b/json/json-3/src/main/scala/io/sphere/json/catsinstances/package.scala deleted file mode 100644 index 779fd45d..00000000 --- a/json/json-3/src/main/scala/io/sphere/json/catsinstances/package.scala +++ /dev/null @@ -1,41 +0,0 @@ -package io.sphere.json - -import _root_.cats.{Contravariant, Functor, Invariant} -import org.json4s.JValue - -/** Cats instances for [[JSON]], [[FromJSON]] and [[ToJSON]] - */ -package object catsinstances extends JSONInstances with FromJSONInstances with ToJSONInstances - -trait JSONInstances { - implicit val catsInvariantForJSON: Invariant[JSON] = new JSONInvariant -} - -trait FromJSONInstances { - implicit val catsFunctorForFromJSON: Functor[FromJSON] = new FromJSONFunctor -} - -trait ToJSONInstances { - implicit val catsContravariantForToJSON: Contravariant[ToJSON] = new ToJSONContravariant -} - -class JSONInvariant extends Invariant[JSON] { - override def imap[A, B](fa: JSON[A])(f: A => B)(g: B => A): JSON[B] = new JSON[B] { - override def write(b: B): JValue = fa.write(g(b)) - override def read(jval: JValue): JValidation[B] = fa.read(jval).map(f) - override val fields: Set[String] = fa.fields - } -} - -class FromJSONFunctor extends Functor[FromJSON] { - override def map[A, B](fa: FromJSON[A])(f: A => B): FromJSON[B] = new FromJSON[B] { - override def read(jval: JValue): JValidation[B] = fa.read(jval).map(f) - override val fields: Set[String] = fa.fields - } -} - -class ToJSONContravariant extends Contravariant[ToJSON] { - override def contramap[A, B](fa: ToJSON[A])(f: B => A): ToJSON[B] = new ToJSON[B] { - override def write(b: B): JValue = fa.write(f(b)) - } -} diff --git a/json/json-3/src/main/scala/io/sphere/json/package.scala b/json/json-3/src/main/scala/io/sphere/json/package.scala deleted file mode 100644 index cc1d4101..00000000 --- a/json/json-3/src/main/scala/io/sphere/json/package.scala +++ /dev/null @@ -1,116 +0,0 @@ -package io.sphere - -import cats.data.Validated.{Invalid, Valid} -import cats.data.{NonEmptyList, ValidatedNel} -import com.fasterxml.jackson.core.JsonParseException -import com.fasterxml.jackson.core.exc.{InputCoercionException, StreamConstraintsException} -import com.fasterxml.jackson.databind.JsonMappingException -import io.sphere.util.Logging -import org.json4s.{DefaultFormats, JsonInput, StringInput} -import org.json4s.JsonAST._ -import org.json4s.ParserUtil.ParseException -import org.json4s.jackson.compactJson -import java.time.format.DateTimeFormatter - -/** Provides functions for reading & writing JSON, via type classes JSON/JSONR/JSONW. */ -package object json extends Logging { - - private[json] val JavaYearMonthFormatter = - DateTimeFormatter.ofPattern("uuuu-MM") - - implicit val liftJsonFormats: DefaultFormats = DefaultFormats - - type JValidation[A] = ValidatedNel[JSONError, A] - - def parseJsonUnsafe(json: JsonInput): JValue = - SphereJsonParser.parse(json, useBigDecimalForDouble = false, useBigIntForLong = false) - - def parseJSON(json: JsonInput): JValidation[JValue] = - try Valid(parseJsonUnsafe(json)) - catch { - case e: ParseException => jsonParseError(e.getMessage) - case e: JsonMappingException => jsonParseError(e.getOriginalMessage) - case e: JsonParseException => jsonParseError(e.getOriginalMessage) - case e: InputCoercionException => jsonParseError(e.getOriginalMessage) - case e: StreamConstraintsException => jsonParseError(e.getOriginalMessage) - } - - def parseJSON(json: String): JValidation[JValue] = - parseJSON(StringInput(json)) - - def jsonParseError[A](msg: String): Invalid[NonEmptyList[JSONError]] = - Invalid(NonEmptyList.one(JSONParseError(msg))) - - def fromJSON[A: FromJSON](json: JsonInput): JValidation[A] = - parseJSON(json).andThen(fromJValue[A]) - - def fromJSON[A: FromJSON](json: String): JValidation[A] = - parseJSON(json).andThen(fromJValue[A]) - - private val jNothingStr = "{}" - - def toJSON[A: ToJSON](a: A): String = toJValue(a) match { - case JNothing => jNothingStr - case jval => compactJson(jval) - } - - /** Parses a JSON string into a type A. Throws a [[JSONException]] on failure. - * - * @param json - * The JSON string to parse. - * @return - * An instance of type A. - */ - def getFromJSON[A: FromJSON](json: JsonInput): A = - getFromJValue[A](parseJsonUnsafe(json)) - - def getFromJSON[A: FromJSON](json: String): A = - getFromJSON(StringInput(json)) - - def fromJValue[A](jval: JValue)(implicit json: FromJSON[A]): JValidation[A] = - json.read(jval) - - def toJValue[A](a: A)(implicit json: ToJSON[A]): JValue = - json.write(a) - - def getFromJValue[A: FromJSON](jval: JValue): A = - fromJValue[A](jval) match { - case Valid(a) => a - case Invalid(errs) => throw new JSONException(errs.toList.mkString(", ")) - } - - /** Extracts a JSON value of type A from a named field of a JSON object. - * - * @param name - * The name of the field. - * @param jObject - * The JObject from which to extract the field. - * @return - * A success with a value of type A or a non-empty list of errors. - */ - def field[A]( - name: String, - default: Option[A] = None - )(jObject: JObject)(implicit jsonr: FromJSON[A]): JValidation[A] = { - val fields = jObject.obj - // Perf note: avoiding Some(f) with fields.indexWhere and then constant time access is not faster - fields.find(f => f._1 == name && f._2 != JNull && f._2 != JNothing) match { - case Some(f) => - jsonr - .read(f._2) - .leftMap(errs => - errs.map { - case JSONParseError(msg) => JSONFieldError(name :: Nil, msg) - case JSONFieldError(path, msg) => JSONFieldError(name :: path, msg) - }) - case None => - default - .map(Valid(_)) - .orElse( - jsonr.read(JNothing).fold(_ => None, x => Some(Valid(x))) - ) // orElse(jsonr.default) - .getOrElse( - Invalid(NonEmptyList.one(JSONFieldError(name :: Nil, "Missing required value")))) - } - } -} diff --git a/json/json-3/src/test/scala/io/sphere/json/BigNumberParsingSpec.scala b/json/json-3/src/test/scala/io/sphere/json/BigNumberParsingSpec.scala deleted file mode 100644 index 11c192fe..00000000 --- a/json/json-3/src/test/scala/io/sphere/json/BigNumberParsingSpec.scala +++ /dev/null @@ -1,24 +0,0 @@ -package io.sphere.json - -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class BigNumberParsingSpec extends AnyWordSpec with Matchers { - import BigNumberParsingSpec._ - - "parsing a big number" should { - "not take much time when parsed as Double" in { - fromJSON[Double](bigNumberAsString).isValid should be(false) - } - "not take much time when parsed as Long" in { - fromJSON[Long](bigNumberAsString).isValid should be(false) - } - "not take much time when parsed as Int" in { - fromJSON[Int](bigNumberAsString).isValid should be(false) - } - } -} - -object BigNumberParsingSpec { - private val bigNumberAsString = "9" * 10000000 -} diff --git a/json/json-3/src/test/scala/io/sphere/json/DateTimeParsingSpec.scala b/json/json-3/src/test/scala/io/sphere/json/DateTimeParsingSpec.scala deleted file mode 100644 index 562e9c3b..00000000 --- a/json/json-3/src/test/scala/io/sphere/json/DateTimeParsingSpec.scala +++ /dev/null @@ -1,173 +0,0 @@ -package io.sphere.json - -import org.json4s.JString -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec -import java.time.Instant -import cats.data.Validated.Valid - -class DateTimeParsingSpec extends AnyWordSpec with Matchers { - - import FromJSON.dateTimeReader - import FromJSON.javaInstantReader - def jsonDateStringWith( - year: String = "2035", - dayOfTheMonth: String = "23", - monthOfTheYear: String = "11", - hourOfTheDay: String = "13", - minuteOfTheHour: String = "45", - secondOfTheMinute: String = "34", - millis: String = "543"): JString = - JString( - s"$year-$monthOfTheYear-${dayOfTheMonth}T$hourOfTheDay:$minuteOfTheHour:$secondOfTheMinute.${millis}Z") - - val beValid = be(Symbol("valid")) - val outOfIntRange = "999999999999999" - - "parsing a DateTime" should { - - "reject strings with invalid year" in { - dateTimeReader.read(jsonDateStringWith(year = "999999999")) shouldNot beValid - } - - "reject strings with years that are out of range for integers" in { - dateTimeReader.read(jsonDateStringWith(year = outOfIntRange)) shouldNot beValid - } - - "reject strings that are out of range for other fields" in { - dateTimeReader.read( - jsonDateStringWith( - monthOfTheYear = outOfIntRange, - dayOfTheMonth = outOfIntRange, - hourOfTheDay = outOfIntRange, - minuteOfTheHour = outOfIntRange, - secondOfTheMinute = outOfIntRange, - millis = outOfIntRange - )) shouldNot beValid - } - - "reject strings with invalid days" in { - dateTimeReader.read(jsonDateStringWith(dayOfTheMonth = "59")) shouldNot beValid - } - - "reject strings with invalid months" in { - dateTimeReader.read(jsonDateStringWith(monthOfTheYear = "39")) shouldNot beValid - } - - "reject strings with invalid hours" in { - dateTimeReader.read(jsonDateStringWith(hourOfTheDay = "39")) shouldNot beValid - } - - "reject strings with invalid minutes" in { - dateTimeReader.read(jsonDateStringWith(minuteOfTheHour = "87")) shouldNot beValid - } - - "reject strings with invalid seconds" in { - dateTimeReader.read(jsonDateStringWith(secondOfTheMinute = "87")) shouldNot beValid - } - } - - "parsing an Instant" should { - - "reject strings with invalid year" in { - javaInstantReader.read(jsonDateStringWith(year = "999999999")) shouldNot beValid - } - - "reject strings with years that are out of range for integers" in { - javaInstantReader.read(jsonDateStringWith(year = outOfIntRange)) shouldNot beValid - } - - "reject strings that are out of range for other fields" in { - javaInstantReader.read( - jsonDateStringWith( - monthOfTheYear = outOfIntRange, - dayOfTheMonth = outOfIntRange, - hourOfTheDay = outOfIntRange, - minuteOfTheHour = outOfIntRange, - secondOfTheMinute = outOfIntRange, - millis = outOfIntRange - )) shouldNot beValid - } - - "reject strings with invalid days" in { - javaInstantReader.read(jsonDateStringWith(dayOfTheMonth = "59")) shouldNot beValid - } - - "reject strings with invalid months" in { - javaInstantReader.read(jsonDateStringWith(monthOfTheYear = "39")) shouldNot beValid - } - - "reject strings with invalid hours" in { - javaInstantReader.read(jsonDateStringWith(hourOfTheDay = "39")) shouldNot beValid - } - - "reject strings with invalid minutes" in { - javaInstantReader.read(jsonDateStringWith(minuteOfTheHour = "87")) shouldNot beValid - } - - "reject strings with invalid seconds" in { - javaInstantReader.read(jsonDateStringWith(secondOfTheMinute = "87")) shouldNot beValid - } - } - - // ported from https://github.com/JodaOrg/joda-time/blob/4a1402a47cab4636bf4c73d42a62bfa80c1535ca/src/test/java/org/joda/time/convert/TestStringConverter.java#L114-L156 - // ensures that we accept similar patterns as joda when parsing instants - "parsing a Java instant" should { - "accept a full instant with milliseconds and offset" in { - javaInstantReader.read(JString("2004-06-09T12:24:48.501+0800")) shouldBe Valid( - Instant.parse("2004-06-09T04:24:48.501Z")) - } - - "accept a year with offset" in { - javaInstantReader.read(JString("2004T+0800")) shouldBe Valid( - Instant.parse("2004-01-01T00:00:00+08:00")) - } - - "accept a year month with offset" in { - javaInstantReader.read(JString("2004-06T+0800")) shouldBe Valid( - Instant.parse("2004-06-01T00:00:00+08:00")) - } - - "accept a year month day with offset" in { - javaInstantReader.read(JString("2004-06-09T+0800")) shouldBe Valid( - Instant.parse("2004-06-09T00:00:00+08:00")) - } - - "accept a year month day with hour and offset" in { - javaInstantReader.read(JString("2004-06-09T12+0800")) shouldBe Valid( - Instant.parse("2004-06-09T04:00:00Z")) - } - - "accept a year month day with hour, minute, and offset" in { - javaInstantReader.read(JString("2004-06-09T12:24+0800")) shouldBe Valid( - Instant.parse("2004-06-09T04:24:00Z")) - } - - "accept a year month day with hour, minute, second, and offset" in { - javaInstantReader.read(JString("2004-06-09T12:24:48+0800")) shouldBe Valid( - Instant.parse("2004-06-09T04:24:48Z")) - } - - "accept a year month day with hour, fraction, and offset" in { - javaInstantReader.read(JString("2004-06-09T12.5+0800")) shouldBe Valid( - Instant.parse("2004-06-09T04:00:00.5Z")) - } - - "accept a year month day with hour, minute, fraction, and offset" in { - javaInstantReader.read(JString("2004-06-09T12:24.5+0800")) shouldBe Valid( - Instant.parse("2004-06-09T04:24:00.5Z")) - } - - "accept a year month day with hour, minute, second, fraction, and offset" in { - javaInstantReader.read(JString("2004-06-09T12:24:48.5+0800")) shouldBe Valid( - Instant.parse("2004-06-09T04:24:48.5Z")) - } - - "accept a year month day with hour, minute, second, fraction, but no offset" in { - javaInstantReader.read(JString("2004-06-09T12:24:48.501")) shouldBe Valid( - Instant.parse("2004-06-09T12:24:48.501Z")) - } - - } - -} diff --git a/json/json-3/src/test/scala/io/sphere/json/JSONProperties.scala b/json/json-3/src/test/scala/io/sphere/json/JSONProperties.scala deleted file mode 100644 index 40ba1b5b..00000000 --- a/json/json-3/src/test/scala/io/sphere/json/JSONProperties.scala +++ /dev/null @@ -1,170 +0,0 @@ -package io.sphere.json - -import scala.language.higherKinds -import io.sphere.util.Money -import java.util.{Currency, Locale, UUID} - -import cats.Eq -import cats.data.NonEmptyList -import cats.syntax.eq._ -import org.joda.time._ -import org.scalacheck._ -import java.time - -import scala.math.BigDecimal.RoundingMode - -object JSONProperties extends Properties("JSON") { - private def check[A: FromJSON: ToJSON: Eq](a: A): Boolean = { - val json = s"""[${toJSON(a)}]""" - val result = fromJSON[Seq[A]](json).toOption.map(_.head).get - val r = result === a - if (!r) println(s"result: $result - expected: $a") - r - } - - implicit def arbitraryVector[A: Arbitrary]: Arbitrary[Vector[A]] = - Arbitrary(Arbitrary.arbitrary[List[A]].map(_.toVector)) - - implicit def arbitraryNEL[A: Arbitrary]: Arbitrary[NonEmptyList[A]] = - Arbitrary(for { - a <- Arbitrary.arbitrary[A] - l <- Arbitrary.arbitrary[List[A]] - } yield NonEmptyList(a, l)) - - implicit def arbitraryCurrency: Arbitrary[Currency] = - Arbitrary(Gen - .oneOf(Currency.getInstance("EUR"), Currency.getInstance("USD"), Currency.getInstance("JPY"))) - - implicit def arbitraryLocale: Arbitrary[Locale] = { - // Filter because OS X thinks that 'C' and 'POSIX' are valid locales... - val locales = Locale.getAvailableLocales().filter(_.toLanguageTag() != "und") - Arbitrary(for { - i <- Gen.choose(0, locales.length - 1) - } yield locales(i)) - } - - implicit def arbitraryDateTime: Arbitrary[DateTime] = - Arbitrary(for { - y <- Gen.choose(-4000, 4000) - m <- Gen.choose(1, 12) - d <- Gen.choose(1, 28) - h <- Gen.choose(0, 23) - min <- Gen.choose(0, 59) - s <- Gen.choose(0, 59) - ms <- Gen.choose(0, 999) - } yield new DateTime(y, m, d, h, min, s, ms, DateTimeZone.UTC)) - - // generate dates between years -4000 and +4000 - implicit val javaInstant: Arbitrary[time.Instant] = - Arbitrary(Gen.choose(-188395027761000L, 64092207599999L).map(time.Instant.ofEpochMilli(_))) - - implicit val javaLocalTime: Arbitrary[time.LocalTime] = Arbitrary( - Gen.choose(0, 3600 * 24).map(time.LocalTime.ofSecondOfDay(_))) - - implicit def arbitraryDate: Arbitrary[LocalDate] = - Arbitrary(Arbitrary.arbitrary[DateTime].map(_.toLocalDate)) - - implicit def arbitraryTime: Arbitrary[LocalTime] = - Arbitrary(Arbitrary.arbitrary[DateTime].map(_.toLocalTime)) - - implicit def arbitraryYearMonth: Arbitrary[YearMonth] = - Arbitrary(Arbitrary.arbitrary[DateTime].map(dt => new YearMonth(dt.getYear, dt.getMonthOfYear))) - - implicit def arbitraryMoney: Arbitrary[Money] = - Arbitrary(for { - c <- Arbitrary.arbitrary[Currency] - i <- Arbitrary.arbitrary[Int] - } yield Money.fromDecimalAmount(i, c)(RoundingMode.HALF_EVEN)) - - implicit def arbitraryUUID: Arbitrary[UUID] = - Arbitrary(for { - most <- Arbitrary.arbitrary[Long] - least <- Arbitrary.arbitrary[Long] - } yield new UUID(most, least)) - - implicit val currencyEqual: Eq[Currency] = new Eq[Currency] { - def eqv(c1: Currency, c2: Currency) = c1.getCurrencyCode == c2.getCurrencyCode - } - implicit val localeEqual: Eq[Locale] = new Eq[Locale] { - def eqv(l1: Locale, l2: Locale) = l1.toLanguageTag == l2.toLanguageTag - } - implicit val moneyEqual: Eq[Money] = new Eq[Money] { - override def eqv(x: Money, y: Money): Boolean = x == y - } - implicit val dateTimeEqual: Eq[DateTime] = new Eq[DateTime] { - def eqv(dt1: DateTime, dt2: DateTime) = dt1 == dt2 - } - implicit val localTimeEqual: Eq[LocalTime] = new Eq[LocalTime] { - def eqv(dt1: LocalTime, dt2: LocalTime) = dt1 == dt2 - } - implicit val localDateEqual: Eq[LocalDate] = new Eq[LocalDate] { - def eqv(dt1: LocalDate, dt2: LocalDate) = dt1 == dt2 - } - implicit val yearMonthEqual: Eq[YearMonth] = new Eq[YearMonth] { - def eqv(dt1: YearMonth, dt2: YearMonth) = dt1 == dt2 - } - implicit val javaInstantEqual: Eq[time.Instant] = Eq.fromUniversalEquals - implicit val javaLocalDateEqual: Eq[time.LocalDate] = Eq.fromUniversalEquals - implicit val javaLocalTimeEqual: Eq[time.LocalTime] = Eq.fromUniversalEquals - implicit val javaYearMonthEqual: Eq[time.YearMonth] = Eq.fromUniversalEquals - - private def checkC[C[_]](name: String)(implicit - jri: FromJSON[C[Int]], - jwi: ToJSON[C[Int]], - arbi: Arbitrary[C[Int]], - eqi: Eq[C[Int]], - jrs: FromJSON[C[Short]], - jws: ToJSON[C[Short]], - arbs: Arbitrary[C[Short]], - eqs: Eq[C[Short]], - jrl: FromJSON[C[Long]], - jwl: ToJSON[C[Long]], - arbl: Arbitrary[C[Long]], - eql: Eq[C[Long]], - jrss: FromJSON[C[String]], - jwss: ToJSON[C[String]], - arbss: Arbitrary[C[String]], - eqss: Eq[C[String]], - jrf: FromJSON[C[Float]], - jwf: ToJSON[C[Float]], - arbf: Arbitrary[C[Float]], - eqf: Eq[C[Float]], - jrd: FromJSON[C[Double]], - jwd: ToJSON[C[Double]], - arbd: Arbitrary[C[Double]], - eqd: Eq[C[Double]], - jrb: FromJSON[C[Boolean]], - jwb: ToJSON[C[Boolean]], - arbb: Arbitrary[C[Boolean]], - eqb: Eq[C[Boolean]] - ) = { - property(s"read/write $name of Ints") = Prop.forAll((l: C[Int]) => check(l)) - property(s"read/write $name of Shorts") = Prop.forAll((l: C[Short]) => check(l)) - property(s"read/write $name of Longs") = Prop.forAll((l: C[Long]) => check(l)) - property(s"read/write $name of Strings") = Prop.forAll((l: C[String]) => check(l)) - property(s"read/write $name of Floats") = Prop.forAll((l: C[Float]) => check(l)) - property(s"read/write $name of Doubles") = Prop.forAll((l: C[Double]) => check(l)) - property(s"read/write $name of Booleans") = Prop.forAll((l: C[Boolean]) => check(l)) - } - - checkC[List]("List") - checkC[Vector]("Vector") - checkC[Set]("Set") - checkC[NonEmptyList]("NonEmptyList") - checkC[Option]("Option") - checkC[({ type l[v] = Map[String, v] })#l]("Map") - - property("read/write Unit") = Prop.forAll((u: Unit) => check(u)) - property("read/write Currency") = Prop.forAll((c: Currency) => check(c)) - property("read/write Money") = Prop.forAll((m: Money) => check(m)) - property("read/write Locale") = Prop.forAll((l: Locale) => check(l)) - property("read/write UUID") = Prop.forAll((u: UUID) => check(u)) - property("read/write DateTime") = Prop.forAll((u: DateTime) => check(u)) - property("read/write LocalDate") = Prop.forAll((u: LocalDate) => check(u)) - property("read/write LocalTime") = Prop.forAll((u: LocalTime) => check(u)) - property("read/write YearMonth") = Prop.forAll((u: YearMonth) => check(u)) - property("read/write java.time.Instant") = Prop.forAll((i: time.Instant) => check(i)) - property("read/write java.time.LocalDate") = Prop.forAll((d: time.LocalDate) => check(d)) - property("read/write java.time.LocalTime") = Prop.forAll((t: time.LocalTime) => check(t)) - property("read/write java.time.YearMonth") = Prop.forAll((ym: time.YearMonth) => check(ym)) -} diff --git a/json/json-3/src/test/scala/io/sphere/json/JodaJavaTimeCompat.scala b/json/json-3/src/test/scala/io/sphere/json/JodaJavaTimeCompat.scala deleted file mode 100644 index 21437cfb..00000000 --- a/json/json-3/src/test/scala/io/sphere/json/JodaJavaTimeCompat.scala +++ /dev/null @@ -1,68 +0,0 @@ -package io.sphere.json - -import org.joda.time.DateTime -import org.joda.time.DateTimeZone -import org.joda.time.LocalDate -import org.joda.time.LocalTime -import org.joda.time.YearMonth -import org.scalacheck.Arbitrary -import org.scalacheck.Gen -import org.scalacheck.Prop -import org.scalacheck.Properties -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -import java.time.{Instant => JInstant} -import java.time.{LocalDate => JLocalDate} -import java.time.{LocalTime => JLocalTime} -import java.time.{YearMonth => JYearMonth} -import cats.data.Validated - -class JodaJavaTimeCompat extends Properties("Joda - java.time compat") { - val epochMillis = Gen.choose(-188395027761000L, 64092207599999L) - - implicit def arbitraryDateTime: Arbitrary[DateTime] = - Arbitrary(epochMillis.map(new DateTime(_, DateTimeZone.UTC))) - - // generate dates between years -4000 and +4000 - implicit val javaInstant: Arbitrary[JInstant] = - Arbitrary(epochMillis.map(JInstant.ofEpochMilli(_))) - - implicit val javaLocalTime: Arbitrary[JLocalTime] = Arbitrary( - Gen.choose(0, 3600 * 24).map(JLocalTime.ofSecondOfDay(_))) - - property("compatibility between serialized Instant and DateTime") = Prop.forAll { - (instant: JInstant) => - val dateTime = new DateTime(instant.toEpochMilli(), DateTimeZone.UTC) - val serializedInstant = ToJSON[JInstant].write(instant) - val serializedDateTime = ToJSON[DateTime].write(dateTime) - serializedInstant == serializedDateTime - } - - property("compatibility between serialized java.time.LocalTime and org.joda.time.LocalTime") = - Prop.forAll { (javaTime: JLocalTime) => - val jodaTime = LocalTime.fromMillisOfDay(javaTime.toNanoOfDay() / 1000000) - val serializedJavaTime = ToJSON[JLocalTime].write(javaTime) - val serializedJodaTime = ToJSON[LocalTime].write(jodaTime) - serializedJavaTime == serializedJodaTime - } - - property("roundtrip from java.time.Instant") = Prop.forAll { (instant: JInstant) => - FromJSON[DateTime] - .read(ToJSON[JInstant].write(instant)) - .andThen { dateTime => - FromJSON[JInstant].read(ToJSON[DateTime].write(dateTime)) - } - .fold(_ => false, _ == instant) - } - - property("roundtrip from org.joda.time.DateTime") = Prop.forAll { (dateTime: DateTime) => - FromJSON[JInstant] - .read(ToJSON[DateTime].write(dateTime)) - .andThen { instant => - FromJSON[DateTime].read(ToJSON[JInstant].write(instant)) - } - .fold(_ => false, _ == dateTime) - } - -} diff --git a/json/json-3/src/test/scala/io/sphere/json/MoneyMarshallingSpec.scala b/json/json-3/src/test/scala/io/sphere/json/MoneyMarshallingSpec.scala deleted file mode 100644 index 048971d5..00000000 --- a/json/json-3/src/test/scala/io/sphere/json/MoneyMarshallingSpec.scala +++ /dev/null @@ -1,110 +0,0 @@ -package io.sphere.json - -import java.util.Currency - -import io.sphere.util.{BaseMoney, HighPrecisionMoney, Money} -import cats.data.Validated.Valid -import org.json4s.jackson.compactJson -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class MoneyMarshallingSpec extends AnyWordSpec with Matchers { - "money encoding/decoding" should { - "be symmetric" in { - val money = Money.EUR(34.56) - val jsonAst = toJValue(money) - val jsonAsString = compactJson(jsonAst) - val Valid(readAst) = parseJSON(jsonAsString): @unchecked - - jsonAst should equal(readAst) - } - - "decode with type info" in { - val json = - """ - { - "type" : "centPrecision", - "currencyCode" : "USD", - "centAmount" : 3298 - } - """ - - fromJSON[BaseMoney](json) should be(Valid(Money.USD(BigDecimal("32.98")))) - } - - "decode without type info" in { - val json = - """ - { - "currencyCode" : "USD", - "centAmount" : 3298 - } - """ - - fromJSON[BaseMoney](json) should be(Valid(Money.USD(BigDecimal("32.98")))) - } - } - - "High precision money encoding/decoding" should { - "be symmetric" in { - implicit val mode = BigDecimal.RoundingMode.HALF_EVEN - - val money = HighPrecisionMoney.fromDecimalAmount(34.123456, 6, Currency.getInstance("EUR")) - val jsonAst = toJValue(money) - val jsonAsString = compactJson(jsonAst) - val Valid(readAst) = parseJSON(jsonAsString): @unchecked - val Valid(decodedMoney) = fromJSON[HighPrecisionMoney](jsonAsString): @unchecked - val Valid(decodedBaseMoney) = fromJSON[BaseMoney](jsonAsString): @unchecked - - jsonAst should equal(readAst) - decodedMoney should equal(money) - decodedBaseMoney should equal(money) - } - - "decode with type info" in { - val json = - """ - { - "type": "highPrecision", - "currencyCode": "USD", - "preciseAmount": 42, - "fractionDigits": 4 - } - """ - - fromJSON[BaseMoney](json) should be( - Valid(HighPrecisionMoney.USD(BigDecimal("0.0042"), Some(4)))) - } - - "decode with centAmount" in { - val Valid(json) = parseJSON(""" - { - "type": "highPrecision", - "currencyCode": "USD", - "preciseAmount": 42, - "centAmount": 1, - "fractionDigits": 4 - } - """): @unchecked - - val Valid(parsed) = fromJValue[BaseMoney](json): @unchecked - - toJValue(parsed) should be(json) - } - - "validate data when decoded from JSON" in { - val json = - """ - { - "type": "highPrecision", - "currencyCode": "USD", - "preciseAmount": 42, - "fractionDigits": 1 - } - """ - - fromJSON[BaseMoney](json).isValid should be(false) - } - } - -} diff --git a/json/json-3/src/test/scala/io/sphere/json/SetHandlingSpec.scala b/json/json-3/src/test/scala/io/sphere/json/SetHandlingSpec.scala deleted file mode 100644 index 74a2d069..00000000 --- a/json/json-3/src/test/scala/io/sphere/json/SetHandlingSpec.scala +++ /dev/null @@ -1,17 +0,0 @@ -package io.sphere.json - -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class SetHandlingSpec extends AnyWordSpec with Matchers { - "JSON deserialization" must { - - "should accept same elements in array to create a set" in { - val jeans = getFromJSON[Set[String]](""" - ["mobile", "mobile"] - """) - - jeans must be(Set("mobile")) - } - } -} diff --git a/json/json-3/src/test/scala/io/sphere/json/SphereJsonExample.scala b/json/json-3/src/test/scala/io/sphere/json/SphereJsonExample.scala deleted file mode 100644 index 69cd62ac..00000000 --- a/json/json-3/src/test/scala/io/sphere/json/SphereJsonExample.scala +++ /dev/null @@ -1,46 +0,0 @@ -package io.sphere.json - -import io.sphere.json._ -import org.json4s.{JObject, JValue} -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class SphereJsonExample extends AnyWordSpec with Matchers { - - case class User(name: String, age: Int, location: String) - - object User { - - // update https://github.com/commercetools/sphere-scala-libs/blob/master/json/README.md in case of changed - implicit val json: JSON[User] = new JSON[User] { - import cats.data.ValidatedNel - import cats.syntax.apply._ - - def read(jval: JValue): ValidatedNel[JSONError, User] = jval match { - case o: JObject => - (field[String]("name")(o), field[Int]("age")(o), field[String]("location")(o)) - .mapN(User.apply) - case _ => fail("JSON object expected.") - } - - def write(u: User): JValue = JObject( - List( - "name" -> toJValue(u.name), - "age" -> toJValue(u.age), - "location" -> toJValue(u.location) - )) - } - } - - "JSON[User]" should { - "serialize and deserialize an user" in { - val user = User("name", 23, "earth") - val json = toJSON(user) - parseJSON(json).isValid should be(true) - - val newUser = getFromJSON[User](json) - newUser should be(user) - } - } - -} diff --git a/json/json-3/src/test/scala/io/sphere/json/SphereJsonParserSpec.scala b/json/json-3/src/test/scala/io/sphere/json/SphereJsonParserSpec.scala deleted file mode 100644 index 024348fd..00000000 --- a/json/json-3/src/test/scala/io/sphere/json/SphereJsonParserSpec.scala +++ /dev/null @@ -1,14 +0,0 @@ -package io.sphere.json - -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class SphereJsonParserSpec extends AnyWordSpec with Matchers { - "Object mapper" must { - - "accept strings with 20_000_000 bytes" in { - SphereJsonParser.mapper.getFactory.streamReadConstraints().getMaxStringLength must be( - 20000000) - } - } -} diff --git a/json/json-3/src/test/scala/io/sphere/json/ToJSONSpec.scala b/json/json-3/src/test/scala/io/sphere/json/ToJSONSpec.scala deleted file mode 100644 index acaeea18..00000000 --- a/json/json-3/src/test/scala/io/sphere/json/ToJSONSpec.scala +++ /dev/null @@ -1,34 +0,0 @@ -package io.sphere.json - -import java.util.UUID - -import org.json4s._ -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class ToJSONSpec extends AnyWordSpec with Matchers { - - case class User(id: UUID, firstName: String, age: Int) - - "ToJSON.apply" must { - "create a ToJSON" in { - implicit val encodeUser: ToJSON[User] = ToJSON.instance[User](u => - JObject( - List( - "id" -> toJValue(u.id), - "first_name" -> toJValue(u.firstName), - "age" -> toJValue(u.age) - ))) - - val id = UUID.randomUUID() - val json = toJValue(User(id, "bidule", 109)) - json must be( - JObject( - List( - "id" -> JString(id.toString), - "first_name" -> JString("bidule"), - "age" -> JLong(109) - ))) - } - } -} diff --git a/json/json-3/src/test/scala/io/sphere/json/catsinstances/JSONCatsInstancesTest.scala b/json/json-3/src/test/scala/io/sphere/json/catsinstances/JSONCatsInstancesTest.scala deleted file mode 100644 index 01c483fb..00000000 --- a/json/json-3/src/test/scala/io/sphere/json/catsinstances/JSONCatsInstancesTest.scala +++ /dev/null @@ -1,53 +0,0 @@ -package io.sphere.json.catsinstances - -import cats.syntax.invariant._ -import cats.syntax.functor._ -import cats.syntax.contravariant._ -import io.sphere.json.JSON -import io.sphere.json._ -import org.json4s.JsonAST -import org.json4s.JsonAST.JString -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class JSONCatsInstancesTest extends AnyWordSpec with Matchers { - import JSONCatsInstancesTest._ - - "Invariant[JSON]" must { - "allow imaping a default format" in { - val myId = MyId("test") - val json = toJValue(myId) - json must be(JString("test")) - val myNewId = getFromJValue[MyId](json) - myNewId must be(myId) - } - } - - "Functor[FromJson] and Contramap[ToJson]" must { - "allow mapping and contramapping a default format" in { - val myId = MyId2("test") - val json = toJValue(myId) - json must be(JString("test")) - val myNewId = getFromJValue[MyId2](json) - myNewId must be(myId) - } - } -} - -object JSONCatsInstancesTest { - private val stringJson: JSON[String] = new JSON[String] { - override def write(value: String): JsonAST.JValue = ToJSON[String].write(value) - override def read(jval: JsonAST.JValue): JValidation[String] = FromJSON[String].read(jval) - } - - case class MyId(id: String) extends AnyVal - object MyId { - implicit val json: JSON[MyId] = stringJson.imap(MyId.apply)(_.id) - } - - case class MyId2(id: String) extends AnyVal - object MyId2 { - implicit val fromJson: FromJSON[MyId2] = FromJSON[String].map(apply) - implicit val toJson: ToJSON[MyId2] = ToJSON[String].contramap(_.id) - } -} diff --git a/json/json-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala b/json/json-core/src/test/scala-3/io/sphere/json/DeriveSingletonJSONSpec.scala similarity index 100% rename from json/json-3/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala rename to json/json-core/src/test/scala-3/io/sphere/json/DeriveSingletonJSONSpec.scala diff --git a/json/json-3/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala b/json/json-core/src/test/scala-3/io/sphere/json/JSONEmbeddedSpec.scala similarity index 100% rename from json/json-3/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala rename to json/json-core/src/test/scala-3/io/sphere/json/JSONEmbeddedSpec.scala diff --git a/json/json-3/src/test/scala/io/sphere/json/NullHandlingSpec.scala b/json/json-core/src/test/scala-3/io/sphere/json/NullHandlingSpec.scala similarity index 100% rename from json/json-3/src/test/scala/io/sphere/json/NullHandlingSpec.scala rename to json/json-core/src/test/scala-3/io/sphere/json/NullHandlingSpec.scala diff --git a/json/json-3/src/test/scala/io/sphere/json/OptionReaderSpec.scala b/json/json-core/src/test/scala-3/io/sphere/json/OptionReaderSpec.scala similarity index 100% rename from json/json-3/src/test/scala/io/sphere/json/OptionReaderSpec.scala rename to json/json-core/src/test/scala-3/io/sphere/json/OptionReaderSpec.scala diff --git a/json/json-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala b/json/json-core/src/test/scala-3/io/sphere/json/generic/DefaultValuesSpec.scala similarity index 100% rename from json/json-3/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala rename to json/json-core/src/test/scala-3/io/sphere/json/generic/DefaultValuesSpec.scala diff --git a/json/json-3/src/test/scala/io/sphere/json/generic/JSONKeySpec.scala b/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONKeySpec.scala similarity index 100% rename from json/json-3/src/test/scala/io/sphere/json/generic/JSONKeySpec.scala rename to json/json-core/src/test/scala-3/io/sphere/json/generic/JSONKeySpec.scala diff --git a/json/json-3/src/test/scala/io/sphere/json/JSONSpec.scala b/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala similarity index 99% rename from json/json-3/src/test/scala/io/sphere/json/JSONSpec.scala rename to json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala index 0bc584cd..8a764031 100644 --- a/json/json-3/src/test/scala/io/sphere/json/JSONSpec.scala +++ b/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala @@ -1,4 +1,4 @@ -package io.sphere.json +package io.sphere.json.generic import cats.data.Validated.{Invalid, Valid} import cats.data.ValidatedNel @@ -6,8 +6,9 @@ import cats.syntax.apply.* import org.json4s.JsonAST.* import io.sphere.json.field import io.sphere.json.generic.* +import io.sphere.json.* import io.sphere.util.Money -import org.joda.time.* +import org.joda.time.DateTime import org.scalatest.matchers.must.Matchers import org.scalatest.funspec.AnyFunSpec import org.json4s.DefaultJsonFormats.given diff --git a/json/json-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala b/json/json-core/src/test/scala-3/io/sphere/json/generic/JsonTypeHintFieldSpec.scala similarity index 100% rename from json/json-3/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala rename to json/json-core/src/test/scala-3/io/sphere/json/generic/JsonTypeHintFieldSpec.scala diff --git a/json/json-3/src/test/scala/io/sphere/json/generic/JsonTypeSwitchSpec.scala b/json/json-core/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala similarity index 100% rename from json/json-3/src/test/scala/io/sphere/json/generic/JsonTypeSwitchSpec.scala rename to json/json-core/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala From 422c4d12d3796d3b653b3afbf7c481f4a8a71857 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 11 Apr 2025 14:32:15 +0200 Subject: [PATCH 071/142] Fixing ci.yml --- .github/workflows/ci.yml | 4 ++-- build.sbt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d2b9e2b..b45ba44d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,10 +56,10 @@ jobs: - name: Build Scala 3 project if: matrix.scala == '3.3.5' - run: sbt '++ ${{ matrix.scala }}' sphere-util/test sphere-mongo-3/test sphere-json-3/test + run: sbt '++ ${{ matrix.scala }}' sphere-util/test sphere-mongo-3/test sphere-json-core/test - name: Compress target directories - run: tar cf targets.tar benchmarks/target mongo/target json/json-core/target mongo/mongo-core/target json/target util/target mongo/mongo-3/target json/json-derivation/target mongo/mongo-derivation-magnolia/target target json/json-3/target mongo/mongo-derivation/target project/target + run: tar cf targets.tar benchmarks/target mongo/target json/json-core/target mongo/mongo-core/target json/target util/target mongo/mongo-3/target json/json-derivation/target mongo/mongo-derivation-magnolia/target target mongo/mongo-derivation/target project/target - name: Upload target directories uses: actions/upload-artifact@v4 diff --git a/build.sbt b/build.sbt index f01f45d8..a681593c 100644 --- a/build.sbt +++ b/build.sbt @@ -37,7 +37,7 @@ ThisBuild / githubWorkflowBuild := Seq( commands = List( "sphere-util/test", "sphere-mongo-3/test", - "sphere-json-3/test" + "sphere-json-core/test" ), name = Some("Build Scala 3 project"), cond = Some(s"matrix.scala == '$scala3'") From 8c2342eb2892d10b87054cf24774481ed59854b8 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 11 Apr 2025 15:52:10 +0200 Subject: [PATCH 072/142] move main parts of mongo-3 to mongo-core --- .github/workflows/ci.yml | 2 +- build.sbt | 30 ++-- .../mongo/catsinstances/catsinstances.scala | 2 +- .../mongo/format/DefaultMongoFormats.scala | 13 +- .../io/sphere/mongo/format/MongoFormat.scala | 4 +- .../mongo/format/DefaultMongoFormats.scala | 6 +- .../io/sphere/mongo/format/MongoFormat.scala | 6 +- .../mongo/generic/AnnotationReader.scala | 0 .../io/sphere/mongo/generic/Annotations.scala | 0 .../io/sphere/mongo/generic/generic.scala | 0 .../sphere/mongo/catsinstances/package.scala | 2 +- .../format/BaseMoneyMongoFormatTest.scala | 0 .../format/DefaultMongoFormatsTest.scala | 0 .../format/BaseMoneyMongoFormatTest.scala | 98 ++++++++++++ .../format/DefaultMongoFormatsTest.scala | 150 ++++++++++++++++++ .../MongoFormatCatsInstancesTest.scala | 2 +- 16 files changed, 276 insertions(+), 39 deletions(-) rename mongo/mongo-core/src/main/{scala => scala-2}/io/sphere/mongo/format/DefaultMongoFormats.scala (98%) rename mongo/mongo-core/src/main/{scala => scala-2}/io/sphere/mongo/format/MongoFormat.scala (91%) rename mongo/{mongo-3/src/main/scala => mongo-core/src/main/scala-3}/io/sphere/mongo/format/DefaultMongoFormats.scala (98%) rename mongo/{mongo-3/src/main/scala => mongo-core/src/main/scala-3}/io/sphere/mongo/format/MongoFormat.scala (96%) rename mongo/{mongo-3/src/main/scala => mongo-core/src/main/scala-3}/io/sphere/mongo/generic/AnnotationReader.scala (100%) rename mongo/{mongo-3/src/main/scala => mongo-core/src/main/scala-3}/io/sphere/mongo/generic/Annotations.scala (100%) rename mongo/{mongo-3/src/main/scala => mongo-core/src/main/scala-3}/io/sphere/mongo/generic/generic.scala (100%) rename mongo/mongo-core/src/test/{scala => scala-2}/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala (100%) rename mongo/mongo-core/src/test/{scala => scala-2}/io/sphere/mongo/format/DefaultMongoFormatsTest.scala (100%) create mode 100644 mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala create mode 100644 mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/DefaultMongoFormatsTest.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b45ba44d..07d7f98f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,7 @@ jobs: run: sbt '++ ${{ matrix.scala }}' sphere-util/test sphere-mongo-3/test sphere-json-core/test - name: Compress target directories - run: tar cf targets.tar benchmarks/target mongo/target json/json-core/target mongo/mongo-core/target json/target util/target mongo/mongo-3/target json/json-derivation/target mongo/mongo-derivation-magnolia/target target mongo/mongo-derivation/target project/target + run: tar cf targets.tar benchmarks/target mongo/target json/json-core/target mongo/mongo-core/target json/target util/target json/json-derivation/target mongo/mongo-derivation-magnolia/target target mongo/mongo-derivation/target project/target - name: Upload target directories uses: actions/upload-artifact@v4 diff --git a/build.sbt b/build.sbt index a681593c..81d44216 100644 --- a/build.sbt +++ b/build.sbt @@ -36,7 +36,7 @@ ThisBuild / githubWorkflowBuild := Seq( WorkflowStep.Sbt( commands = List( "sphere-util/test", - "sphere-mongo-3/test", + "sphere-mongo-core/test", "sphere-json-core/test" ), name = Some("Build Scala 3 project"), @@ -102,10 +102,6 @@ lazy val `sphere-libs` = project .settings(standardSettings: _*) .settings(publishArtifact := false, publish := {}, crossScalaVersions := Seq()) .aggregate( - // Scala 3 modules - `sphere-mongo-3`, - - // Scala 2 modules `sphere-util`, `sphere-json`, `sphere-json-core`, @@ -117,15 +113,7 @@ lazy val `sphere-libs` = project `benchmarks` ) -// Scala 3 modules - -lazy val `sphere-mongo-3` = project - .settings(scalaVersion := scala3) - .in(file("./mongo/mongo-3")) - .settings(standardSettings: _*) - .dependsOn(`sphere-util`) - -// Scala 2 modules +// Scala 2 & 3 modules lazy val `sphere-util` = project .in(file("./util")) @@ -139,6 +127,14 @@ lazy val `sphere-json-core` = project .settings(crossScalaVersions := Seq(scala212, scala213, scala3)) .dependsOn(`sphere-util`) +lazy val `sphere-mongo-core` = project + .in(file("./mongo/mongo-core")) + .settings(standardSettings: _*) + .settings(crossScalaVersions := Seq(scala212, scala213, scala3)) + .dependsOn(`sphere-util`) + +// Scala 2 modules + lazy val `sphere-json-derivation` = project .in(file("./json/json-derivation")) .settings(standardSettings: _*) @@ -154,12 +150,6 @@ lazy val `sphere-json` = project .settings(crossScalaVersions := Seq(scala212, scala213)) .dependsOn(`sphere-json-core`, `sphere-json-derivation`) -lazy val `sphere-mongo-core` = project - .in(file("./mongo/mongo-core")) - .settings(standardSettings: _*) - .settings(crossScalaVersions := Seq(scala212, scala213)) - .dependsOn(`sphere-util`) - lazy val `sphere-mongo-derivation` = project .in(file("./mongo/mongo-derivation")) .settings(standardSettings: _*) diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/catsinstances/catsinstances.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/catsinstances/catsinstances.scala index d1e9cde6..3c20fdd5 100644 --- a/mongo/mongo-3/src/main/scala/io/sphere/mongo/catsinstances/catsinstances.scala +++ b/mongo/mongo-3/src/main/scala/io/sphere/mongo/catsinstances/catsinstances.scala @@ -17,6 +17,6 @@ class MongoFormatInvariant extends Invariant[MongoFormat] { new MongoFormat[B] { override def toMongoValue(b: B): Any = fa.toMongoValue(g(b)) override def fromMongoValue(any: Any): B = f(fa.fromMongoValue(any)) - override val fieldNames: Vector[String] = fa.fieldNames + override val fields: Vector[String] = fa.fieldNames } } diff --git a/mongo/mongo-core/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala b/mongo/mongo-core/src/main/scala-2/io/sphere/mongo/format/DefaultMongoFormats.scala similarity index 98% rename from mongo/mongo-core/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala rename to mongo/mongo-core/src/main/scala-2/io/sphere/mongo/format/DefaultMongoFormats.scala index 199343fb..eb77313f 100644 --- a/mongo/mongo-core/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala +++ b/mongo/mongo-core/src/main/scala-2/io/sphere/mongo/format/DefaultMongoFormats.scala @@ -1,12 +1,11 @@ package io.sphere.mongo.format -import java.util.{Currency, Locale, UUID} -import java.util.regex.Pattern - import io.sphere.util.{BaseMoney, HighPrecisionMoney, LangTag, Money} -import org.bson.{BSONObject, BasicBSONObject} import org.bson.types.{BasicBSONList, ObjectId} +import org.bson.{BSONObject, BasicBSONObject} +import java.util.regex.Pattern +import java.util.{Currency, Locale, UUID} import scala.collection.immutable.VectorBuilder import scala.collection.mutable.ListBuffer @@ -189,7 +188,7 @@ trait DefaultMongoFormats { implicit val moneyFormat: MongoFormat[Money] = new MongoFormat[Money] { import Money._ - override val fields = Set(CentAmountField, CurrencyCodeField) + override val fields = Vector(CentAmountField, CurrencyCodeField) override def toMongoValue(m: Money): Any = new BasicBSONObject() @@ -209,9 +208,9 @@ trait DefaultMongoFormats { implicit val highPrecisionMoneyFormat: MongoFormat[HighPrecisionMoney] = new MongoFormat[HighPrecisionMoney] { - import HighPrecisionMoney._ + import HighPrecisionMoney._ - override val fields = Set(PreciseAmountField, CurrencyCodeField, FractionDigitsField) + override val fields = Vector(PreciseAmountField, CurrencyCodeField, FractionDigitsField) override def toMongoValue(m: HighPrecisionMoney): Any = new BasicBSONObject() diff --git a/mongo/mongo-core/src/main/scala/io/sphere/mongo/format/MongoFormat.scala b/mongo/mongo-core/src/main/scala-2/io/sphere/mongo/format/MongoFormat.scala similarity index 91% rename from mongo/mongo-core/src/main/scala/io/sphere/mongo/format/MongoFormat.scala rename to mongo/mongo-core/src/main/scala-2/io/sphere/mongo/format/MongoFormat.scala index 7179cd2b..921d6fb3 100644 --- a/mongo/mongo-core/src/main/scala/io/sphere/mongo/format/MongoFormat.scala +++ b/mongo/mongo-core/src/main/scala-2/io/sphere/mongo/format/MongoFormat.scala @@ -13,12 +13,12 @@ trait MongoFormat[@specialized A] extends Serializable { def default: Option[A] = None /** needed JSON fields - ignored if empty */ - val fields: Set[String] = MongoFormat.emptyFieldsSet + val fields: Vector[String] = MongoFormat.emptyFields } object MongoFormat extends MongoFormatInstances { - private[MongoFormat] val emptyFieldsSet: Set[String] = Set.empty + private[MongoFormat] val emptyFields: Vector[String] = Vector.empty @inline def apply[A](implicit instance: MongoFormat[A]): MongoFormat[A] = instance diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/DefaultMongoFormats.scala similarity index 98% rename from mongo/mongo-3/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala rename to mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/DefaultMongoFormats.scala index 0c14edd0..9f483ecb 100644 --- a/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/DefaultMongoFormats.scala @@ -46,7 +46,7 @@ trait DefaultMongoFormats { } override def fromMongoValue(mongoType: Any): Option[A] = { - val fieldNames = format.fieldNames + val fieldNames = format.fields if (mongoType == null) None else mongoType match { @@ -178,7 +178,7 @@ trait DefaultMongoFormats { given moneyFormat: MongoFormat[Money] = new MongoFormat[Money] { import Money._ - override val fieldNames = Vector(CentAmountField, CurrencyCodeField) + override val fields = Vector(CentAmountField, CurrencyCodeField) override def toMongoValue(m: Money): Any = new BasicBSONObject() @@ -200,7 +200,7 @@ trait DefaultMongoFormats { new MongoFormat[HighPrecisionMoney] { import HighPrecisionMoney._ - override val fieldNames = Vector(PreciseAmountField, CurrencyCodeField, FractionDigitsField) + override val fields = Vector(PreciseAmountField, CurrencyCodeField, FractionDigitsField) override def toMongoValue(m: HighPrecisionMoney): Any = new BasicBSONObject() diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/MongoFormat.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala similarity index 96% rename from mongo/mongo-3/src/main/scala/io/sphere/mongo/format/MongoFormat.scala rename to mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala index 2b36dade..173d5fb2 100644 --- a/mongo/mongo-3/src/main/scala/io/sphere/mongo/format/MongoFormat.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala @@ -18,7 +18,7 @@ trait MongoFormat[A] extends Serializable { def fromMongoValue(mongoType: Any): A // /** needed JSON fields - ignored if empty */ - val fieldNames: Vector[String] = MongoFormat.emptyFields + val fields: Vector[String] = MongoFormat.emptyFields def default: Option[A] = None } @@ -95,8 +95,8 @@ object MongoFormat { private val formatters = summonFormatters[mirrorOfProduct.MirroredElemTypes] private val fieldsAndFormatters = caseClassMetaData.fields.zip(formatters) - override val fieldNames: Vector[String] = fieldsAndFormatters.flatMap((field, formatter) => - if (field.embedded) formatter.fieldNames :+ field.rawName + override val fields: Vector[String] = fieldsAndFormatters.flatMap((field, formatter) => + if (field.embedded) formatter.fields :+ field.rawName else Vector(field.rawName)) override def toMongoValue(a: A): Any = { diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/AnnotationReader.scala similarity index 100% rename from mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/AnnotationReader.scala rename to mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/AnnotationReader.scala diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/Annotations.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/Annotations.scala similarity index 100% rename from mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/Annotations.scala rename to mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/Annotations.scala diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/generic.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala similarity index 100% rename from mongo/mongo-3/src/main/scala/io/sphere/mongo/generic/generic.scala rename to mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala diff --git a/mongo/mongo-core/src/main/scala/io/sphere/mongo/catsinstances/package.scala b/mongo/mongo-core/src/main/scala/io/sphere/mongo/catsinstances/package.scala index 19053718..4e115654 100644 --- a/mongo/mongo-core/src/main/scala/io/sphere/mongo/catsinstances/package.scala +++ b/mongo/mongo-core/src/main/scala/io/sphere/mongo/catsinstances/package.scala @@ -17,6 +17,6 @@ class MongoFormatInvariant extends Invariant[MongoFormat] { new MongoFormat[B] { override def toMongoValue(b: B): Any = fa.toMongoValue(g(b)) override def fromMongoValue(any: Any): B = f(fa.fromMongoValue(any)) - override val fields: Set[String] = fa.fields + override val fields: Vector[String] = fa.fields } } diff --git a/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala b/mongo/mongo-core/src/test/scala-2/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala similarity index 100% rename from mongo/mongo-core/src/test/scala/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala rename to mongo/mongo-core/src/test/scala-2/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala diff --git a/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/DefaultMongoFormatsTest.scala b/mongo/mongo-core/src/test/scala-2/io/sphere/mongo/format/DefaultMongoFormatsTest.scala similarity index 100% rename from mongo/mongo-core/src/test/scala/io/sphere/mongo/format/DefaultMongoFormatsTest.scala rename to mongo/mongo-core/src/test/scala-2/io/sphere/mongo/format/DefaultMongoFormatsTest.scala diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala new file mode 100644 index 00000000..9b66bdc7 --- /dev/null +++ b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala @@ -0,0 +1,98 @@ +package io.sphere.mongo.format + +import java.util.Currency + +import io.sphere.util.{BaseMoney, HighPrecisionMoney, Money} +import DefaultMongoFormats.given +import io.sphere.mongo.MongoUtils._ +import org.bson.BSONObject +import org.scalatest.wordspec.AnyWordSpec +import org.scalatest.matchers.should.Matchers + +import scala.collection.JavaConverters._ + +class BaseMoneyMongoFormatTest extends AnyWordSpec with Matchers { + + "MongoFormat[BaseMoney]" should { + "be symmetric" in { + val money = Money.EUR(34.56) + val f = MongoFormat[Money] + val dbo = f.toMongoValue(money) + val readMoney = f.fromMongoValue(dbo) + + money should be(readMoney) + } + + "decode with type info" in { + val dbo = dbObj( + "type" -> "centPrecision", + "currencyCode" -> "USD", + "centAmount" -> 3298 + ) + + MongoFormat[BaseMoney].fromMongoValue(dbo) should be(Money.USD(BigDecimal("32.98"))) + } + + "decode without type info" in { + val dbo = dbObj( + "currencyCode" -> "USD", + "centAmount" -> 3298 + ) + + MongoFormat[BaseMoney].fromMongoValue(dbo) should be(Money.USD(BigDecimal("32.98"))) + } + } + + "MongoFormat[HighPrecisionMoney]" should { + "be symmetric" in { + implicit val mode = BigDecimal.RoundingMode.HALF_EVEN + + val money = HighPrecisionMoney.fromDecimalAmount(34.123456, 6, Currency.getInstance("EUR")) + val dbo = MongoFormat[HighPrecisionMoney].toMongoValue(money) + + val decodedMoney = MongoFormat[HighPrecisionMoney].fromMongoValue(dbo) + val decodedBaseMoney = MongoFormat[BaseMoney].fromMongoValue(dbo) + + decodedMoney should equal(money) + decodedBaseMoney should equal(money) + } + + "decode with type info" in { + val dbo = dbObj( + "type" -> "highPrecision", + "currencyCode" -> "USD", + "preciseAmount" -> 42, + "fractionDigits" -> 4 + ) + + MongoFormat[BaseMoney].fromMongoValue(dbo) should be( + HighPrecisionMoney.USD(BigDecimal("0.0042"), Some(4))) + } + + "decode with centAmount" in { + val dbo = dbObj( + "type" -> "highPrecision", + "currencyCode" -> "USD", + "preciseAmount" -> 42, + "centAmount" -> 1, + "fractionDigits" -> 4 + ) + + val parsed = MongoFormat[BaseMoney].fromMongoValue(dbo) + MongoFormat[BaseMoney].toMongoValue(parsed).asInstanceOf[BSONObject].toMap.asScala should be( + dbo.toMap.asScala) + } + + "validate data when decoded from JSON" in { + val dbo = dbObj( + "type" -> "highPrecision", + "currencyCode" -> "USD", + "preciseAmount" -> 42, + "fractionDigits" -> 1 + ) + + an[Exception] shouldBe thrownBy(MongoFormat[BaseMoney].fromMongoValue(dbo)) + } + } + +} diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/DefaultMongoFormatsTest.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/DefaultMongoFormatsTest.scala new file mode 100644 index 00000000..5d483ee8 --- /dev/null +++ b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/DefaultMongoFormatsTest.scala @@ -0,0 +1,150 @@ +package io.sphere.mongo.format + +import java.util.Locale +import com.mongodb.DBObject +import io.sphere.mongo.MongoUtils +import io.sphere.mongo.format.DefaultMongoFormats.given +import io.sphere.util.LangTag +import org.bson.BasicBSONObject +import org.bson.types.BasicBSONList +import org.scalacheck.Gen +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec +import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks + +import scala.collection.JavaConverters._ + +object DefaultMongoFormatsTest { + case class User(name: String) + object User { + implicit val mongo: MongoFormat[User] = new MongoFormat[User] { + override def toMongoValue(a: User): Any = MongoUtils.dbObj("name" -> a.name) + override def fromMongoValue(any: Any): User = any match { + case dbo: DBObject => + User(dbo.get("name").asInstanceOf[String]) + case _ => throw new Exception("expected DBObject") + } + } + } +} + +class DefaultMongoFormatsTest + extends AnyWordSpec + with Matchers + with ScalaCheckDrivenPropertyChecks { + import DefaultMongoFormatsTest._ + + "DefaultMongoFormats" must { + "support List[String]" in { + val format = listFormat[String] + val list = Gen.listOf(Gen.alphaNumStr) + + forAll(list) { l => + val dbo = format.toMongoValue(l) + dbo.asInstanceOf[BasicBSONList].asScala.toList must be(l) + val resultList = format.fromMongoValue(dbo) + resultList must be(l) + } + } + + "support List[A: MongoFormat]" in { + val format = listFormat[User] + val list = Gen.listOf(Gen.alphaNumStr.map(User.apply)) + + check(list, format) + } + + "support Vector[String]" in { + val format = vecFormat[String] + val vector = Gen.listOf(Gen.alphaNumStr).map(_.toVector) + + forAll(vector) { v => + val dbo = format.toMongoValue(v) + dbo.asInstanceOf[BasicBSONList].asScala.toVector must be(v) + val resultVector = format.fromMongoValue(dbo) + resultVector must be(v) + } + } + + "support Vector[A: MongoFormat]" in { + val format = vecFormat[User] + val vector = Gen.listOf(Gen.alphaNumStr.map(User.apply)).map(_.toVector) + + check(vector, format) + } + + "support Set[String]" in { + val format = setFormat[String] + val set = Gen.listOf(Gen.alphaNumStr).map(_.toSet) + + forAll(set) { s => + val dbo = format.toMongoValue(s) + dbo.asInstanceOf[BasicBSONList].asScala.toSet must be(s) + val resultSet = format.fromMongoValue(dbo) + resultSet must be(s) + } + } + + "support Set[A: MongoFormat]" in { + val format = setFormat[User] + val set = Gen.listOf(Gen.alphaNumStr.map(User.apply)).map(_.toSet) + + check(set, format) + } + + "support Map[String, String]" in { + val format = mapFormat[String] + val map = Gen + .listOf { + for { + key <- Gen.alphaNumStr + value <- Gen.alphaNumStr + } yield (key, value) + } + .map(_.toMap) + + forAll(map) { m => + val dbo = format.toMongoValue(m) + dbo.asInstanceOf[BasicBSONObject].asScala must be(m) + val resultMap = format.fromMongoValue(dbo) + resultMap must be(m) + } + } + + "support Map[String, A: MongoFormat]" in { + val format = mapFormat[User] + val map = Gen + .listOf { + for { + key <- Gen.alphaNumStr + value <- Gen.alphaNumStr.map(User.apply) + } yield (key, value) + } + .map(_.toMap) + + check(map, format) + } + + "support Java Locale" in { + Locale.getAvailableLocales.filter(_.toLanguageTag != LangTag.UNDEFINED).foreach { + (l: Locale) => + localeFormat.fromMongoValue(localeFormat.toMongoValue(l)).toLanguageTag must be( + l.toLanguageTag) + } + } + + "support UUID" in { + val format = uuidFormat + val uuids = Gen.uuid + + check(uuids, format) + } + } + + private def check[A](gen: Gen[A], format: MongoFormat[A]) = + forAll(gen) { value => + val dbo = format.toMongoValue(value) + val result = format.fromMongoValue(dbo) + result must be(value) + } +} diff --git a/mongo/mongo-core/src/test/scala/io/sphere/mongo/catsinstances/MongoFormatCatsInstancesTest.scala b/mongo/mongo-core/src/test/scala/io/sphere/mongo/catsinstances/MongoFormatCatsInstancesTest.scala index 47f37647..8ea87eb9 100644 --- a/mongo/mongo-core/src/test/scala/io/sphere/mongo/catsinstances/MongoFormatCatsInstancesTest.scala +++ b/mongo/mongo-core/src/test/scala/io/sphere/mongo/catsinstances/MongoFormatCatsInstancesTest.scala @@ -1,7 +1,7 @@ package io.sphere.mongo.catsinstances import cats.syntax.invariant._ -import io.sphere.mongo.format.DefaultMongoFormats._ +import io.sphere.mongo.format.DefaultMongoFormats.stringFormat import io.sphere.mongo.format._ import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec From 23c468f87f4c43ded0ae3464513df08dc3c63b0e Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 11 Apr 2025 17:33:45 +0200 Subject: [PATCH 073/142] Move tests from mongo-3 --- mongo/mongo-3/dependencies.sbt | 3 - .../mongo/catsinstances/catsinstances.scala | 22 --- .../main/scala/io/sphere/mongo/format.scala | 8 - mongo/mongo-3/src/test/scala/MongoUtils.scala | 10 -- .../MongoFormatCatsInstancesTest.scala | 29 ---- .../format/BaseMoneyMongoFormatTest.scala | 97 ------------ .../format/DefaultMongoFormatsTest.scala | 149 ------------------ .../io/sphere/mongo/DerivationSpec.scala | 0 .../io/sphere/mongo/SerializationTest.scala | 0 .../mongo/format/OptionMongoFormatSpec.scala | 0 .../mongo/generic/DefaultValuesSpec.scala | 0 .../mongo/generic/DeriveMongoFormatSpec.scala | 2 +- .../mongo/generic/MongoEmbeddedSpec.scala | 0 .../mongo/generic/MongoIgnoreSpec.scala | 0 .../sphere/mongo/generic/MongoKeySpec.scala | 0 ...goTypeHintFieldWithAbstractClassSpec.scala | 0 ...ongoTypeHintFieldWithSealedTraitSpec.scala | 0 .../mongo/generic/MongoTypeSwitchSpec.scala | 0 .../mongo/generic/SumTypesDerivingSpec.scala | 0 19 files changed, 1 insertion(+), 319 deletions(-) delete mode 100644 mongo/mongo-3/dependencies.sbt delete mode 100644 mongo/mongo-3/src/main/scala/io/sphere/mongo/catsinstances/catsinstances.scala delete mode 100644 mongo/mongo-3/src/main/scala/io/sphere/mongo/format.scala delete mode 100644 mongo/mongo-3/src/test/scala/MongoUtils.scala delete mode 100644 mongo/mongo-3/src/test/scala/io/sphere/mongo/catsinstances/MongoFormatCatsInstancesTest.scala delete mode 100644 mongo/mongo-3/src/test/scala/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala delete mode 100644 mongo/mongo-3/src/test/scala/io/sphere/mongo/format/DefaultMongoFormatsTest.scala rename mongo/{mongo-3/src/test/scala => mongo-core/src/test/scala-3}/io/sphere/mongo/DerivationSpec.scala (100%) rename mongo/{mongo-3/src/test/scala => mongo-core/src/test/scala-3}/io/sphere/mongo/SerializationTest.scala (100%) rename mongo/{mongo-3/src/test/scala => mongo-core/src/test/scala-3}/io/sphere/mongo/format/OptionMongoFormatSpec.scala (100%) rename mongo/{mongo-3/src/test/scala => mongo-core/src/test/scala-3}/io/sphere/mongo/generic/DefaultValuesSpec.scala (100%) rename mongo/{mongo-3/src/test/scala => mongo-core/src/test/scala-3}/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala (98%) rename mongo/{mongo-3/src/test/scala => mongo-core/src/test/scala-3}/io/sphere/mongo/generic/MongoEmbeddedSpec.scala (100%) rename mongo/{mongo-3/src/test/scala => mongo-core/src/test/scala-3}/io/sphere/mongo/generic/MongoIgnoreSpec.scala (100%) rename mongo/{mongo-3/src/test/scala => mongo-core/src/test/scala-3}/io/sphere/mongo/generic/MongoKeySpec.scala (100%) rename mongo/{mongo-3/src/test/scala => mongo-core/src/test/scala-3}/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala (100%) rename mongo/{mongo-3/src/test/scala => mongo-core/src/test/scala-3}/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala (100%) rename mongo/{mongo-3/src/test/scala => mongo-core/src/test/scala-3}/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala (100%) rename mongo/{mongo-3/src/test/scala => mongo-core/src/test/scala-3}/io/sphere/mongo/generic/SumTypesDerivingSpec.scala (100%) diff --git a/mongo/mongo-3/dependencies.sbt b/mongo/mongo-3/dependencies.sbt deleted file mode 100644 index a25186f0..00000000 --- a/mongo/mongo-3/dependencies.sbt +++ /dev/null @@ -1,3 +0,0 @@ -libraryDependencies ++= Seq( - "org.mongodb" % "mongodb-driver-core" % "5.1.2" // tracking http://mongodb.github.io/mongo-java-driver/ -) diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/catsinstances/catsinstances.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/catsinstances/catsinstances.scala deleted file mode 100644 index 3c20fdd5..00000000 --- a/mongo/mongo-3/src/main/scala/io/sphere/mongo/catsinstances/catsinstances.scala +++ /dev/null @@ -1,22 +0,0 @@ -package io.sphere.mongo - -import _root_.cats.Invariant -import io.sphere.mongo.format.MongoFormat - -/** Cats instances for [[MongoFormat]] - */ -package object catsinstances extends MongoFormatInstances - -trait MongoFormatInstances { - implicit val catsInvariantForMongoFormat: Invariant[MongoFormat] = - new MongoFormatInvariant -} - -class MongoFormatInvariant extends Invariant[MongoFormat] { - override def imap[A, B](fa: MongoFormat[A])(f: A => B)(g: B => A): MongoFormat[B] = - new MongoFormat[B] { - override def toMongoValue(b: B): Any = fa.toMongoValue(g(b)) - override def fromMongoValue(any: Any): B = f(fa.fromMongoValue(any)) - override val fields: Vector[String] = fa.fieldNames - } -} diff --git a/mongo/mongo-3/src/main/scala/io/sphere/mongo/format.scala b/mongo/mongo-3/src/main/scala/io/sphere/mongo/format.scala deleted file mode 100644 index 3483dc12..00000000 --- a/mongo/mongo-3/src/main/scala/io/sphere/mongo/format.scala +++ /dev/null @@ -1,8 +0,0 @@ -package io.sphere.mongo - -import io.sphere.mongo.generic -import io.sphere.mongo.format.MongoFormat - -def toMongo[A: MongoFormat](a: A): Any = summon[MongoFormat[A]].toMongoValue(a) - -def fromMongo[A: MongoFormat](any: Any): A = summon[MongoFormat[A]].fromMongoValue(any) diff --git a/mongo/mongo-3/src/test/scala/MongoUtils.scala b/mongo/mongo-3/src/test/scala/MongoUtils.scala deleted file mode 100644 index 71c641e8..00000000 --- a/mongo/mongo-3/src/test/scala/MongoUtils.scala +++ /dev/null @@ -1,10 +0,0 @@ -package io.sphere.mongo - -import com.mongodb.BasicDBObject - -object MongoUtils { - - def dbObj(pairs: (String, Any)*) = - pairs.foldLeft(new BasicDBObject) { case (obj, (key, value)) => obj.append(key, value) } - -} diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/catsinstances/MongoFormatCatsInstancesTest.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/catsinstances/MongoFormatCatsInstancesTest.scala deleted file mode 100644 index 34fbb491..00000000 --- a/mongo/mongo-3/src/test/scala/io/sphere/mongo/catsinstances/MongoFormatCatsInstancesTest.scala +++ /dev/null @@ -1,29 +0,0 @@ -package io.sphere.mongo.catsinstances - -import cats.syntax.invariant.* -import io.sphere.mongo.{fromMongo, toMongo} -import io.sphere.mongo.format.* -import io.sphere.mongo.format.DefaultMongoFormats.given -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class MongoFormatCatsInstancesTest extends AnyWordSpec with Matchers { - import MongoFormatCatsInstancesTest.* - - "Invariant[MongoFormat]" must { - "allow imaping a default format" in { - val myId = MyId("test") - val dbo = toMongo(myId) - dbo.asInstanceOf[String] must be("test") - val myNewId = fromMongo[MyId](dbo) - myNewId must be(myId) - } - } -} - -object MongoFormatCatsInstancesTest { - case class MyId(id: String) extends AnyVal - object MyId { - implicit val mongo: MongoFormat[MyId] = MongoFormat[String].imap(MyId.apply)(_.id) - } -} diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala deleted file mode 100644 index aeac2be2..00000000 --- a/mongo/mongo-3/src/test/scala/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala +++ /dev/null @@ -1,97 +0,0 @@ -package io.sphere.mongo.format - -import io.sphere.mongo.MongoUtils.* -import io.sphere.mongo.format.DefaultMongoFormats.given -import io.sphere.util.{BaseMoney, HighPrecisionMoney, Money} -import org.bson.BSONObject -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -import java.util.Currency -import scala.jdk.CollectionConverters.* - -class BaseMoneyMongoFormatTest extends AnyWordSpec with Matchers { - - "MongoFormat[BaseMoney]" should { - "be symmetric" in { - val money = Money.EUR(34.56) - val f = MongoFormat[Money] - val dbo = f.toMongoValue(money) - val readMoney = f.fromMongoValue(dbo) - - money should be(readMoney) - } - - "decode with type info" in { - val dbo = dbObj( - "type" -> "centPrecision", - "currencyCode" -> "USD", - "centAmount" -> 3298 - ) - - MongoFormat[BaseMoney].fromMongoValue(dbo) should be(Money.USD(BigDecimal("32.98"))) - } - - "decode without type info" in { - val dbo = dbObj( - "currencyCode" -> "USD", - "centAmount" -> 3298 - ) - - MongoFormat[BaseMoney].fromMongoValue(dbo) should be(Money.USD(BigDecimal("32.98"))) - } - } - - "MongoFormat[HighPrecisionMoney]" should { - "be symmetric" in { - implicit val mode = BigDecimal.RoundingMode.HALF_EVEN - - val money = HighPrecisionMoney.fromDecimalAmount(34.123456, 6, Currency.getInstance("EUR")) - val dbo = MongoFormat[HighPrecisionMoney].toMongoValue(money) - - val decodedMoney = MongoFormat[HighPrecisionMoney].fromMongoValue(dbo) - val decodedBaseMoney = MongoFormat[BaseMoney].fromMongoValue(dbo) - - decodedMoney should equal(money) - decodedBaseMoney should equal(money) - } - - "decode with type info" in { - val dbo = dbObj( - "type" -> "highPrecision", - "currencyCode" -> "USD", - "preciseAmount" -> 42, - "fractionDigits" -> 4 - ) - - MongoFormat[BaseMoney].fromMongoValue(dbo) should be( - HighPrecisionMoney.USD(BigDecimal("0.0042"), Some(4))) - } - - "decode with centAmount" in { - val dbo = dbObj( - "type" -> "highPrecision", - "currencyCode" -> "USD", - "preciseAmount" -> 42, - "centAmount" -> 1, - "fractionDigits" -> 4 - ) - - val parsed = MongoFormat[BaseMoney].fromMongoValue(dbo) - MongoFormat[BaseMoney].toMongoValue(parsed).asInstanceOf[BSONObject].toMap.asScala should be( - dbo.toMap.asScala) - } - - "validate data when decoded from JSON" in { - val dbo = dbObj( - "type" -> "highPrecision", - "currencyCode" -> "USD", - "preciseAmount" -> 42, - "fractionDigits" -> 1 - ) - - an[Exception] shouldBe thrownBy(MongoFormat[BaseMoney].fromMongoValue(dbo)) - } - } - -} diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/format/DefaultMongoFormatsTest.scala b/mongo/mongo-3/src/test/scala/io/sphere/mongo/format/DefaultMongoFormatsTest.scala deleted file mode 100644 index fd97ae26..00000000 --- a/mongo/mongo-3/src/test/scala/io/sphere/mongo/format/DefaultMongoFormatsTest.scala +++ /dev/null @@ -1,149 +0,0 @@ -package io.sphere.mongo.format - -import com.mongodb.DBObject -import io.sphere.mongo.MongoUtils -import io.sphere.mongo.format.DefaultMongoFormats.given -import io.sphere.util.LangTag -import org.bson.BasicBSONObject -import org.bson.types.BasicBSONList -import org.scalacheck.Gen -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec -import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks - -import java.util.Locale -import scala.jdk.CollectionConverters.* - -object DefaultMongoFormatsTest { - case class User(name: String) - object User { - implicit val mongo: MongoFormat[User] = new MongoFormat[User] { - override def toMongoValue(a: User): Any = MongoUtils.dbObj("name" -> a.name) - override def fromMongoValue(any: Any): User = any match { - case dbo: DBObject => - User(dbo.get("name").asInstanceOf[String]) - case _ => throw new Exception("expected DBObject") - } - } - } -} - -class DefaultMongoFormatsTest - extends AnyWordSpec - with Matchers - with ScalaCheckDrivenPropertyChecks { - import DefaultMongoFormatsTest.* - - "DefaultMongoFormats" must { - "support List[String]" in { - val format = listFormat[String] - val list = Gen.listOf(Gen.alphaNumStr) - - forAll(list) { l => - val dbo = format.toMongoValue(l) - dbo.asInstanceOf[BasicBSONList].asScala.toList must be(l) - val resultList = format.fromMongoValue(dbo) - resultList must be(l) - } - } - - "support List[A: MongoFormat]" in { - val format = listFormat[User] - val list = Gen.listOf(Gen.alphaNumStr.map(User.apply)) - - check(list, format) - } - - "support Vector[String]" in { - val format = vecFormat[String] - val vector = Gen.listOf(Gen.alphaNumStr).map(_.toVector) - - forAll(vector) { v => - val dbo = format.toMongoValue(v) - dbo.asInstanceOf[BasicBSONList].asScala.toVector must be(v) - val resultVector = format.fromMongoValue(dbo) - resultVector must be(v) - } - } - - "support Vector[A: MongoFormat]" in { - val format = vecFormat[User] - val vector = Gen.listOf(Gen.alphaNumStr.map(User.apply)).map(_.toVector) - - check(vector, format) - } - - "support Set[String]" in { - val format = setFormat[String] - val set = Gen.listOf(Gen.alphaNumStr).map(_.toSet) - - forAll(set) { s => - val dbo = format.toMongoValue(s) - dbo.asInstanceOf[BasicBSONList].asScala.toSet must be(s) - val resultSet = format.fromMongoValue(dbo) - resultSet must be(s) - } - } - - "support Set[A: MongoFormat]" in { - val format = setFormat[User] - val set = Gen.listOf(Gen.alphaNumStr.map(User.apply)).map(_.toSet) - - check(set, format) - } - - "support Map[String, String]" in { - val format = mapFormat[String] - val map = Gen - .listOf { - for { - key <- Gen.alphaNumStr - value <- Gen.alphaNumStr - } yield (key, value) - } - .map(_.toMap) - - forAll(map) { m => - val dbo = format.toMongoValue(m) - dbo.asInstanceOf[BasicBSONObject].asScala must be(m) - val resultMap = format.fromMongoValue(dbo) - resultMap must be(m) - } - } - - "support Map[String, A: MongoFormat]" in { - val format = mapFormat[User] - val map = Gen - .listOf { - for { - key <- Gen.alphaNumStr - value <- Gen.alphaNumStr.map(User.apply) - } yield (key, value) - } - .map(_.toMap) - - check(map, format) - } - - "support Java Locale" in { - Locale.getAvailableLocales.filter(_.toLanguageTag != LangTag.UNDEFINED).foreach { l => - localeFormat.fromMongoValue(localeFormat.toMongoValue(l)).toLanguageTag must be( - l.toLanguageTag) - } - } - - "support UUID" in { - val format = uuidFormat - val uuids = Gen.uuid - - check(uuids, format) - } - } - - private def check[A](gen: Gen[A], format: MongoFormat[A]) = - forAll(gen) { value => - val dbo = format.toMongoValue(value) - val result = format.fromMongoValue(dbo) - result must be(value) - } -} diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/DerivationSpec.scala similarity index 100% rename from mongo/mongo-3/src/test/scala/io/sphere/mongo/DerivationSpec.scala rename to mongo/mongo-core/src/test/scala-3/io/sphere/mongo/DerivationSpec.scala diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/SerializationTest.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/SerializationTest.scala similarity index 100% rename from mongo/mongo-3/src/test/scala/io/sphere/mongo/SerializationTest.scala rename to mongo/mongo-core/src/test/scala-3/io/sphere/mongo/SerializationTest.scala diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/OptionMongoFormatSpec.scala similarity index 100% rename from mongo/mongo-3/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala rename to mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/OptionMongoFormatSpec.scala diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/DefaultValuesSpec.scala similarity index 100% rename from mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala rename to mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/DefaultValuesSpec.scala diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala similarity index 98% rename from mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala rename to mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala index 01fe73bd..7c96930e 100644 --- a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala +++ b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala @@ -5,7 +5,7 @@ import org.scalatest.wordspec.AnyWordSpec import io.sphere.mongo.format.DefaultMongoFormats.given import io.sphere.mongo.MongoUtils.* import io.sphere.mongo.format.{MongoFormat, deriveMongoFormat} -import io.sphere.mongo.{fromMongo, toMongo} +import io.sphere.mongo.format.{fromMongo, toMongo} class DeriveMongoFormatSpec extends AnyWordSpec with Matchers { import DeriveMongoFormatSpec.given diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoEmbeddedSpec.scala similarity index 100% rename from mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala rename to mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoEmbeddedSpec.scala diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoIgnoreSpec.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoIgnoreSpec.scala similarity index 100% rename from mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoIgnoreSpec.scala rename to mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoIgnoreSpec.scala diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoKeySpec.scala similarity index 100% rename from mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala rename to mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoKeySpec.scala diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala similarity index 100% rename from mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala rename to mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala similarity index 100% rename from mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala rename to mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala similarity index 100% rename from mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala rename to mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala diff --git a/mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala similarity index 100% rename from mongo/mongo-3/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala rename to mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala From 86578b5e0e0194e9cc6dad9e64797cdead32e73b Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 11 Apr 2025 17:38:38 +0200 Subject: [PATCH 074/142] formatting --- .../scala-2/io/sphere/mongo/format/DefaultMongoFormats.scala | 2 +- util/dependencies.sbt | 4 ++-- util/src/main/scala/Money.scala | 4 ++-- util/src/test/scala/HighPrecisionMoneySpec.scala | 1 - util/src/test/scala/MoneySpec.scala | 1 - 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/mongo/mongo-core/src/main/scala-2/io/sphere/mongo/format/DefaultMongoFormats.scala b/mongo/mongo-core/src/main/scala-2/io/sphere/mongo/format/DefaultMongoFormats.scala index eb77313f..46acdfab 100644 --- a/mongo/mongo-core/src/main/scala-2/io/sphere/mongo/format/DefaultMongoFormats.scala +++ b/mongo/mongo-core/src/main/scala-2/io/sphere/mongo/format/DefaultMongoFormats.scala @@ -208,7 +208,7 @@ trait DefaultMongoFormats { implicit val highPrecisionMoneyFormat: MongoFormat[HighPrecisionMoney] = new MongoFormat[HighPrecisionMoney] { - import HighPrecisionMoney._ + import HighPrecisionMoney._ override val fields = Vector(PreciseAmountField, CurrencyCodeField, FractionDigitsField) diff --git a/util/dependencies.sbt b/util/dependencies.sbt index 26759f84..f0c76af7 100644 --- a/util/dependencies.sbt +++ b/util/dependencies.sbt @@ -2,6 +2,6 @@ libraryDependencies ++= Seq( "com.typesafe.scala-logging" %% "scala-logging" % "3.9.4", "joda-time" % "joda-time" % "2.13.0", "org.joda" % "joda-convert" % "3.0.1", - ("org.typelevel" %% "cats-core" % "2.13.0"), - ("org.json4s" %% "json4s-scalap" % "4.0.7") + "org.typelevel" %% "cats-core" % "2.13.0", + "org.json4s" %% "json4s-scalap" % "4.0.7" ) diff --git a/util/src/main/scala/Money.scala b/util/src/main/scala/Money.scala index 24b47d97..12bceac7 100644 --- a/util/src/main/scala/Money.scala +++ b/util/src/main/scala/Money.scala @@ -64,7 +64,7 @@ object BaseMoney { def requireSameCurrency(m1: BaseMoney, m2: BaseMoney): Unit = require(m1.currency eq m2.currency, s"${m1.currency} != ${m2.currency}") - def toScalaRoundingMode(mode: java.math.RoundingMode): RoundingMode.Value = + def toScalaRoundingMode(mode: java.math.RoundingMode): RoundingMode = BigDecimal.RoundingMode(mode.ordinal) implicit def baseMoneyMonoid(implicit c: Currency, mode: RoundingMode): Monoid[BaseMoney] = @@ -90,7 +90,7 @@ object BaseMoney { * @param currency * The currency of the amount. */ -case class Money private[util] (centAmount: Long, currency: Currency) +case class Money private (centAmount: Long, currency: Currency) extends BaseMoney with Ordered[Money] { import Money._ diff --git a/util/src/test/scala/HighPrecisionMoneySpec.scala b/util/src/test/scala/HighPrecisionMoneySpec.scala index 6854c0b5..1b80016b 100644 --- a/util/src/test/scala/HighPrecisionMoneySpec.scala +++ b/util/src/test/scala/HighPrecisionMoneySpec.scala @@ -9,7 +9,6 @@ import org.scalatest.matchers.must.Matchers import scala.collection.mutable.ArrayBuffer import scala.language.postfixOps -import scala.math.BigDecimal class HighPrecisionMoneySpec extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks { import HighPrecisionMoney.ImplicitsString._ diff --git a/util/src/test/scala/MoneySpec.scala b/util/src/test/scala/MoneySpec.scala index 7d4c1a4a..9a12f3a3 100644 --- a/util/src/test/scala/MoneySpec.scala +++ b/util/src/test/scala/MoneySpec.scala @@ -5,7 +5,6 @@ import org.scalatest.matchers.must.Matchers import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks import scala.language.postfixOps -import scala.math.BigDecimal class MoneySpec extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyChecks { import Money.ImplicitsDecimal._ From d96edd83cd166a3561005a1d1c4c6cd70b584e28 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 11 Apr 2025 17:42:38 +0200 Subject: [PATCH 075/142] fix ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 07d7f98f..fb767ece 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: - name: Build Scala 3 project if: matrix.scala == '3.3.5' - run: sbt '++ ${{ matrix.scala }}' sphere-util/test sphere-mongo-3/test sphere-json-core/test + run: sbt '++ ${{ matrix.scala }}' sphere-util/test sphere-mongo-core/test sphere-json-core/test - name: Compress target directories run: tar cf targets.tar benchmarks/target mongo/target json/json-core/target mongo/mongo-core/target json/target util/target json/json-derivation/target mongo/mongo-derivation-magnolia/target target mongo/mongo-derivation/target project/target From fe15e403b02a25592c23a6895e6e0145e81c2b8a Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 11 Apr 2025 17:52:27 +0200 Subject: [PATCH 076/142] FromMongo.fields is not a Vector[String] instead of Set[String] because scala3 relies on the order --- .../src/main/scala/io/sphere/mongo/generic/package.scala | 6 +++--- .../main/scala/io/sphere/mongo/generic/package.fmpp.scala | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/package.scala b/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/package.scala index 019a1f85..1c82ef90 100644 --- a/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/package.scala +++ b/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/package.scala @@ -55,9 +55,9 @@ package object generic extends Logging { case _ => sys.error("Deserialization failed. DBObject expected.") } - override val fields: Set[String] = calculateFields() - private def calculateFields(): Set[String] = { - val builder = Set.newBuilder[String] + override val fields: Vector[String] = calculateFields() + private def calculateFields(): Vector[String] = { + val builder = Vector.newBuilder[String] var i = 0 caseClass.parameters.foreach { p => val f = _fields(i) diff --git a/mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/package.fmpp.scala b/mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/package.fmpp.scala index 5d039f1d..fa11b8c4 100644 --- a/mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/package.fmpp.scala +++ b/mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/package.fmpp.scala @@ -93,9 +93,9 @@ package object generic extends Logging { ) case _ => sys.error("Deserialization failed. DBObject expected.") } - override val fields: Set[String] = calculateFields() - private def calculateFields(): Set[String] = { - val builder = Set.newBuilder[String] + override val fields: Vector[String] = calculateFields() + private def calculateFields(): Vector[String] = { + val builder = Vector.newBuilder[String] <#list 1..i as j> val f${j} = _fields(${j-1}) if (!f${j}.ignored) { From eb5e3ca8f25f34d73b3dd5b92648a0dee7316da0 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 11 Apr 2025 17:55:01 +0200 Subject: [PATCH 077/142] remove unnecessary change --- util/src/main/scala/Money.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/src/main/scala/Money.scala b/util/src/main/scala/Money.scala index 12bceac7..0daeea0c 100644 --- a/util/src/main/scala/Money.scala +++ b/util/src/main/scala/Money.scala @@ -65,7 +65,7 @@ object BaseMoney { require(m1.currency eq m2.currency, s"${m1.currency} != ${m2.currency}") def toScalaRoundingMode(mode: java.math.RoundingMode): RoundingMode = - BigDecimal.RoundingMode(mode.ordinal) + BigDecimal.RoundingMode(mode.ordinal()) implicit def baseMoneyMonoid(implicit c: Currency, mode: RoundingMode): Monoid[BaseMoney] = new Monoid[BaseMoney] { From 6db374af063de2c679f70e3532f364cdb7b83399 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 24 Apr 2025 11:21:42 +0200 Subject: [PATCH 078/142] Move back MongoFormatter.fields to Set --- .../io/sphere/mongo/format/DefaultMongoFormats.scala | 4 ++-- .../scala-2/io/sphere/mongo/format/MongoFormat.scala | 4 ++-- .../io/sphere/mongo/format/DefaultMongoFormats.scala | 7 ++++--- .../scala-3/io/sphere/mongo/format/MongoFormat.scala | 10 +++++----- .../scala/io/sphere/mongo/catsinstances/package.scala | 2 +- .../main/scala/io/sphere/mongo/generic/package.scala | 6 +++--- .../scala/io/sphere/mongo/generic/package.fmpp.scala | 6 +++--- 7 files changed, 20 insertions(+), 19 deletions(-) diff --git a/mongo/mongo-core/src/main/scala-2/io/sphere/mongo/format/DefaultMongoFormats.scala b/mongo/mongo-core/src/main/scala-2/io/sphere/mongo/format/DefaultMongoFormats.scala index 46acdfab..2c30f5ab 100644 --- a/mongo/mongo-core/src/main/scala-2/io/sphere/mongo/format/DefaultMongoFormats.scala +++ b/mongo/mongo-core/src/main/scala-2/io/sphere/mongo/format/DefaultMongoFormats.scala @@ -188,7 +188,7 @@ trait DefaultMongoFormats { implicit val moneyFormat: MongoFormat[Money] = new MongoFormat[Money] { import Money._ - override val fields = Vector(CentAmountField, CurrencyCodeField) + override val fields = Set(CentAmountField, CurrencyCodeField) override def toMongoValue(m: Money): Any = new BasicBSONObject() @@ -210,7 +210,7 @@ trait DefaultMongoFormats { new MongoFormat[HighPrecisionMoney] { import HighPrecisionMoney._ - override val fields = Vector(PreciseAmountField, CurrencyCodeField, FractionDigitsField) + override val fields = Set(PreciseAmountField, CurrencyCodeField, FractionDigitsField) override def toMongoValue(m: HighPrecisionMoney): Any = new BasicBSONObject() diff --git a/mongo/mongo-core/src/main/scala-2/io/sphere/mongo/format/MongoFormat.scala b/mongo/mongo-core/src/main/scala-2/io/sphere/mongo/format/MongoFormat.scala index 921d6fb3..0a726c9a 100644 --- a/mongo/mongo-core/src/main/scala-2/io/sphere/mongo/format/MongoFormat.scala +++ b/mongo/mongo-core/src/main/scala-2/io/sphere/mongo/format/MongoFormat.scala @@ -13,12 +13,12 @@ trait MongoFormat[@specialized A] extends Serializable { def default: Option[A] = None /** needed JSON fields - ignored if empty */ - val fields: Vector[String] = MongoFormat.emptyFields + val fields: Set[String] = MongoFormat.emptyFields } object MongoFormat extends MongoFormatInstances { - private[MongoFormat] val emptyFields: Vector[String] = Vector.empty + private[MongoFormat] val emptyFields: Set[String] = Set.empty @inline def apply[A](implicit instance: MongoFormat[A]): MongoFormat[A] = instance diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/DefaultMongoFormats.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/DefaultMongoFormats.scala index 9f483ecb..2fb2eff7 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/DefaultMongoFormats.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/DefaultMongoFormats.scala @@ -46,13 +46,14 @@ trait DefaultMongoFormats { } override def fromMongoValue(mongoType: Any): Option[A] = { + import scala.jdk.CollectionConverters.* val fieldNames = format.fields if (mongoType == null) None else mongoType match { case s: SimpleMongoType => Some(format.fromMongoValue(s)) case bson: BasicDBObject => - val bsonFieldNames = bson.keySet().toArray + val bsonFieldNames = bson.keySet().asScala if (fieldNames.nonEmpty && bsonFieldNames.intersect(fieldNames).isEmpty) None else Some(format.fromMongoValue(bson)) case MongoNothing => None // This can't happen, but it makes the compiler happy @@ -178,7 +179,7 @@ trait DefaultMongoFormats { given moneyFormat: MongoFormat[Money] = new MongoFormat[Money] { import Money._ - override val fields = Vector(CentAmountField, CurrencyCodeField) + override val fields = Set(CentAmountField, CurrencyCodeField) override def toMongoValue(m: Money): Any = new BasicBSONObject() @@ -200,7 +201,7 @@ trait DefaultMongoFormats { new MongoFormat[HighPrecisionMoney] { import HighPrecisionMoney._ - override val fields = Vector(PreciseAmountField, CurrencyCodeField, FractionDigitsField) + override val fields = Set(PreciseAmountField, CurrencyCodeField, FractionDigitsField) override def toMongoValue(m: HighPrecisionMoney): Any = new BasicBSONObject() diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala index 173d5fb2..49001712 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala @@ -18,7 +18,7 @@ trait MongoFormat[A] extends Serializable { def fromMongoValue(mongoType: Any): A // /** needed JSON fields - ignored if empty */ - val fields: Vector[String] = MongoFormat.emptyFields + val fields: Set[String] = MongoFormat.emptyFields def default: Option[A] = None } @@ -32,7 +32,7 @@ inline def deriveMongoFormat[A](using Mirror.Of[A]): MongoFormat[A] = MongoForma object MongoFormat { inline def apply[A: MongoFormat]: MongoFormat[A] = summon - private val emptyFields: Vector[String] = Vector.empty + private val emptyFields: Set[String] = Set.empty inline given derived[A](using Mirror.Of[A]): MongoFormat[A] = Derivation.derived @@ -95,9 +95,9 @@ object MongoFormat { private val formatters = summonFormatters[mirrorOfProduct.MirroredElemTypes] private val fieldsAndFormatters = caseClassMetaData.fields.zip(formatters) - override val fields: Vector[String] = fieldsAndFormatters.flatMap((field, formatter) => - if (field.embedded) formatter.fields :+ field.rawName - else Vector(field.rawName)) + override val fields: Set[String] = fieldsAndFormatters.toSet.flatMap((field, formatter) => + if (field.embedded) formatter.fields + field.rawName + else Set(field.rawName)) override def toMongoValue(a: A): Any = { val bson = new BasicDBObject() diff --git a/mongo/mongo-core/src/main/scala/io/sphere/mongo/catsinstances/package.scala b/mongo/mongo-core/src/main/scala/io/sphere/mongo/catsinstances/package.scala index 4e115654..19053718 100644 --- a/mongo/mongo-core/src/main/scala/io/sphere/mongo/catsinstances/package.scala +++ b/mongo/mongo-core/src/main/scala/io/sphere/mongo/catsinstances/package.scala @@ -17,6 +17,6 @@ class MongoFormatInvariant extends Invariant[MongoFormat] { new MongoFormat[B] { override def toMongoValue(b: B): Any = fa.toMongoValue(g(b)) override def fromMongoValue(any: Any): B = f(fa.fromMongoValue(any)) - override val fields: Vector[String] = fa.fields + override val fields: Set[String] = fa.fields } } diff --git a/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/package.scala b/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/package.scala index 1c82ef90..019a1f85 100644 --- a/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/package.scala +++ b/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/package.scala @@ -55,9 +55,9 @@ package object generic extends Logging { case _ => sys.error("Deserialization failed. DBObject expected.") } - override val fields: Vector[String] = calculateFields() - private def calculateFields(): Vector[String] = { - val builder = Vector.newBuilder[String] + override val fields: Set[String] = calculateFields() + private def calculateFields(): Set[String] = { + val builder = Set.newBuilder[String] var i = 0 caseClass.parameters.foreach { p => val f = _fields(i) diff --git a/mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/package.fmpp.scala b/mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/package.fmpp.scala index fa11b8c4..5d039f1d 100644 --- a/mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/package.fmpp.scala +++ b/mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/package.fmpp.scala @@ -93,9 +93,9 @@ package object generic extends Logging { ) case _ => sys.error("Deserialization failed. DBObject expected.") } - override val fields: Vector[String] = calculateFields() - private def calculateFields(): Vector[String] = { - val builder = Vector.newBuilder[String] + override val fields: Set[String] = calculateFields() + private def calculateFields(): Set[String] = { + val builder = Set.newBuilder[String] <#list 1..i as j> val f${j} = _fields(${j-1}) if (!f${j}.ignored) { From 9363968a2f8436dd37e1a1ddeef9cb34aaa640ca Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 24 Apr 2025 17:37:29 +0200 Subject: [PATCH 079/142] Move FromJSON instances to a common file (between scala 2/3) --- .../scala-2/io/sphere/json/FromJSON.scala | 433 +------------- .../main/scala-2/io/sphere/json/JSON.scala | 2 +- .../main/scala-2/io/sphere/json/ToJSON.scala | 2 +- .../scala-3/io.sphere.json/FromJSON.scala | 535 +----------------- .../main/scala-3/io.sphere.json/JSON.scala | 2 +- .../main/scala-3/io.sphere.json/ToJSON.scala | 2 +- .../io.sphere.json/generic/DeriveJSON.scala | 105 ++++ .../io/sphere/json/FromJSONInstances.scala | 438 ++++++++++++++ .../sphere/json/catsinstances/package.scala | 11 +- 9 files changed, 558 insertions(+), 972 deletions(-) create mode 100644 json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveJSON.scala create mode 100644 json/json-core/src/main/scala/io/sphere/json/FromJSONInstances.scala diff --git a/json/json-core/src/main/scala-2/io/sphere/json/FromJSON.scala b/json/json-core/src/main/scala-2/io/sphere/json/FromJSON.scala index af33533a..ac54ca1a 100644 --- a/json/json-core/src/main/scala-2/io/sphere/json/FromJSON.scala +++ b/json/json-core/src/main/scala-2/io/sphere/json/FromJSON.scala @@ -1,25 +1,8 @@ package io.sphere.json -import scala.util.control.NonFatal -import scala.collection.mutable.ListBuffer -import java.util.{Currency, Locale, UUID} - -import cats.data.NonEmptyList -import cats.data.Validated.{Invalid, Valid} -import cats.syntax.apply._ -import cats.syntax.traverse._ -import io.sphere.json.field -import io.sphere.util.{BaseMoney, HighPrecisionMoney, LangTag, Money} import org.json4s.JsonAST._ -import org.joda.time.format.ISODateTimeFormat import scala.annotation.implicitNotFound -import java.time -import org.joda.time.DateTime -import org.joda.time.DateTimeZone -import org.joda.time.YearMonth -import org.joda.time.LocalTime -import org.joda.time.LocalDate /** Type class for types that can be read from JSON. */ @implicitNotFound("Could not find an instance of FromJSON for ${A}") @@ -31,424 +14,10 @@ trait FromJSON[@specialized A] extends Serializable { val fields: Set[String] = FromJSON.emptyFieldsSet } -object FromJSON extends FromJSONInstances { +object FromJSON extends FromJSONInstances with FromJSONCatsInstances { private[FromJSON] val emptyFieldsSet: Set[String] = Set.empty @inline def apply[A](implicit instance: FromJSON[A]): FromJSON[A] = instance - private val validNone = Valid(None) - private val validNil = Valid(Nil) - private val validEmptyAnyVector: Valid[Vector[Any]] = Valid(Vector.empty) - private def validList[A]: Valid[List[A]] = validNil - private def validEmptyVector[A]: Valid[Vector[A]] = - validEmptyAnyVector.asInstanceOf[Valid[Vector[A]]] - - implicit def optionMapReader[@specialized A](implicit - c: FromJSON[A]): FromJSON[Option[Map[String, A]]] = - new FromJSON[Option[Map[String, A]]] { - private val internalMapReader = mapReader[A] - - def read(jval: JValue): JValidation[Option[Map[String, A]]] = jval match { - case JNothing | JNull => validNone - case x => internalMapReader.read(x).map(Some.apply) - } - } - - implicit def optionReader[@specialized A](implicit c: FromJSON[A]): FromJSON[Option[A]] = - new FromJSON[Option[A]] { - def read(jval: JValue): JValidation[Option[A]] = jval match { - case JNothing | JNull | JObject(Nil) => validNone - case JObject(s) if fields.nonEmpty && s.forall(t => !fields.contains(t._1)) => - validNone // if none of the optional fields are in the JSON - case x => c.read(x).map(Option.apply) - } - override val fields: Set[String] = c.fields - } - - implicit def listReader[@specialized A](implicit r: FromJSON[A]): FromJSON[List[A]] = - new FromJSON[List[A]] { - - def read(jval: JValue): JValidation[List[A]] = jval match { - case JArray(l) => - if (l.isEmpty) validList[A] - else { - // "imperative" style for performances - val errors = new ListBuffer[JSONError]() - val valids = new ListBuffer[A]() - var failedOnce: Boolean = false - l.foreach { jval => - r.read(jval) match { - case Valid(s) if !failedOnce => - valids += s - case Invalid(nel) => - errors ++= nel.toList - failedOnce = true - case _ => () - } - } - if (errors.isEmpty) - Valid(valids.result()) - else - Invalid(NonEmptyList.fromListUnsafe(errors.result())) - } - case _ => fail("JSON Array expected.") - } - } - - implicit def seqReader[@specialized A](implicit r: FromJSON[A]): FromJSON[Seq[A]] = - new FromJSON[Seq[A]] { - def read(jval: JValue): JValidation[Seq[A]] = listReader(r).read(jval) - } - - implicit def setReader[@specialized A](implicit r: FromJSON[A]): FromJSON[Set[A]] = - new FromJSON[Set[A]] { - def read(jval: JValue): JValidation[Set[A]] = jval match { - case JArray(l) => - if (l.isEmpty) Valid(Set.empty) - else listReader(r).read(jval).map(Set.apply(_: _*)) - case _ => fail("JSON Array expected.") - } - } - - implicit def vectorReader[@specialized A](implicit r: FromJSON[A]): FromJSON[Vector[A]] = - new FromJSON[Vector[A]] { - import scala.collection.immutable.VectorBuilder - - def read(jval: JValue): JValidation[Vector[A]] = jval match { - case JArray(l) => - if (l.isEmpty) validEmptyVector - else { - // "imperative" style for performances - val errors = new ListBuffer[JSONError]() - val valids = new VectorBuilder[A]() - var failedOnce: Boolean = false - l.foreach { jval => - r.read(jval) match { - case Valid(s) if !failedOnce => - valids += s - case Invalid(nel) => - errors ++= nel.toList - failedOnce = true - case _ => () - } - } - if (errors.isEmpty) - Valid(valids.result()) - else - Invalid(NonEmptyList.fromListUnsafe(errors.result())) - } - case _ => fail("JSON Array expected.") - } - } - - implicit def nonEmptyListReader[A](implicit r: FromJSON[A]): FromJSON[NonEmptyList[A]] = - new FromJSON[NonEmptyList[A]] { - def read(jval: JValue): JValidation[NonEmptyList[A]] = - fromJValue[List[A]](jval).andThen { - case head :: tail => Valid(NonEmptyList(head, tail)) - case Nil => fail("Non-empty JSON array expected") - } - } - - implicit val intReader: FromJSON[Int] = new FromJSON[Int] { - def read(jval: JValue): JValidation[Int] = jval match { - case JInt(i) if i.isValidInt => Valid(i.toInt) - case JLong(i) if i.isValidInt => Valid(i.toInt) - case _ => fail("JSON Number in the range of an Int expected.") - } - } - - implicit val stringReader: FromJSON[String] = new FromJSON[String] { - def read(jval: JValue): JValidation[String] = jval match { - case JString(s) => Valid(s) - case _ => fail("JSON String expected.") - } - } - - implicit val bigIntReader: FromJSON[BigInt] = new FromJSON[BigInt] { - def read(jval: JValue): JValidation[BigInt] = jval match { - case JInt(i) => Valid(i) - case JLong(l) => Valid(l) - case _ => fail("JSON Number in the range of a BigInt expected.") - } - } - - implicit val shortReader: FromJSON[Short] = new FromJSON[Short] { - def read(jval: JValue): JValidation[Short] = jval match { - case JInt(i) if i.isValidShort => Valid(i.toShort) - case JLong(l) if l.isValidShort => Valid(l.toShort) - case _ => fail("JSON Number in the range of a Short expected.") - } - } - - implicit val longReader: FromJSON[Long] = new FromJSON[Long] { - def read(jval: JValue): JValidation[Long] = jval match { - case JInt(i) => Valid(i.toLong) - case JLong(l) => Valid(l) - case _ => fail("JSON Number in the range of a Long expected.") - } - } - - implicit val floatReader: FromJSON[Float] = new FromJSON[Float] { - def read(jval: JValue): JValidation[Float] = jval match { - case JDouble(d) => Valid(d.toFloat) - case _ => fail("JSON Number in the range of a Float expected.") - } - } - - implicit val doubleReader: FromJSON[Double] = new FromJSON[Double] { - def read(jval: JValue): JValidation[Double] = jval match { - case JDouble(d) => Valid(d) - case JInt(i) => Valid(i.toDouble) - case JLong(l) => Valid(l.toDouble) - case _ => fail("JSON Number in the range of a Double expected.") - } - } - - implicit val booleanReader: FromJSON[Boolean] = new FromJSON[Boolean] { - private val cachedTrue = Valid(true) - private val cachedFalse = Valid(false) - def read(jval: JValue): JValidation[Boolean] = jval match { - case JBool(b) => if (b) cachedTrue else cachedFalse - case _ => fail("JSON Boolean expected") - } - } - - implicit def mapReader[A: FromJSON]: FromJSON[Map[String, A]] = new FromJSON[Map[String, A]] { - def read(json: JValue): JValidation[Map[String, A]] = json match { - case JObject(fs) => - // Perf note: an imperative implementation does not seem faster - fs.traverse[JValidation, (String, A)] { f => - fromJValue[A](f._2).map(v => (f._1, v)) - }.map(_.toMap) - case _ => fail("JSON Object expected") - } - } - - implicit val moneyReader: FromJSON[Money] = new FromJSON[Money] { - import Money._ - - override val fields = Set(CentAmountField, CurrencyCodeField) - - def read(value: JValue): JValidation[Money] = value match { - case o: JObject => - (field[Long](CentAmountField)(o), field[Currency](CurrencyCodeField)(o)) match { - case (Valid(centAmount), Valid(currencyCode)) => - Valid(Money.fromCentAmount(centAmount, currencyCode)) - case (Invalid(e1), Invalid(e2)) => Invalid(e1.concat(e2.toList)) - case (e1 @ Invalid(_), _) => e1 - case (_, e2 @ Invalid(_)) => e2 - } - - case _ => fail("JSON object expected.") - } - } - - implicit val highPrecisionMoneyReader: FromJSON[HighPrecisionMoney] = - new FromJSON[HighPrecisionMoney] { - import HighPrecisionMoney._ - - override val fields = Set(PreciseAmountField, CurrencyCodeField, FractionDigitsField) - - def read(value: JValue): JValidation[HighPrecisionMoney] = value match { - case o: JObject => - val validatedFields = ( - field[Long](PreciseAmountField)(o), - field[Int](FractionDigitsField)(o), - field[Currency](CurrencyCodeField)(o), - field[Option[Long]](CentAmountField)(o)) - - validatedFields.tupled.andThen { - case (preciseAmount, fractionDigits, currencyCode, centAmount) => - HighPrecisionMoney - .fromPreciseAmount(preciseAmount, fractionDigits, currencyCode, centAmount) - .leftMap(_.map(JSONParseError.apply)) - } - - case _ => - fail("JSON object expected.") - } - } - - implicit val baseMoneyReader: FromJSON[BaseMoney] = new FromJSON[BaseMoney] { - def read(value: JValue): JValidation[BaseMoney] = value match { - case o: JObject => - field[Option[String]](BaseMoney.TypeField)(o).andThen { - case None => moneyReader.read(value) - case Some(Money.TypeName) => moneyReader.read(value) - case Some(HighPrecisionMoney.TypeName) => highPrecisionMoneyReader.read(value) - case Some(tpe) => - fail( - s"Unknown money type '$tpe'. Available types are: '${Money.TypeName}', '${HighPrecisionMoney.TypeName}'.") - } - - case _ => fail("JSON object expected.") - } - } - - implicit val currencyReader: FromJSON[Currency] = new FromJSON[Currency] { - val failMsg = "ISO 4217 code JSON String expected." - def failMsgFor(input: String) = s"Currency '$input' not valid as ISO 4217 code." - - private val cachedEUR = Valid(Currency.getInstance("EUR")) - private val cachedUSD = Valid(Currency.getInstance("USD")) - private val cachedGBP = Valid(Currency.getInstance("GBP")) - private val cachedJPY = Valid(Currency.getInstance("JPY")) - - def read(jval: JValue): JValidation[Currency] = jval match { - case JString(s) => - s match { - case "EUR" => cachedEUR - case "USD" => cachedUSD - case "GBP" => cachedGBP - case "JPY" => cachedJPY - case _ => - try Valid(Currency.getInstance(s)) - catch { - case _: IllegalArgumentException => fail(failMsgFor(s)) - } - } - case _ => fail(failMsg) - } - } - - implicit val jValueReader: FromJSON[JValue] = new FromJSON[JValue] { - def read(jval: JValue): JValidation[JValue] = Valid(jval) - } - - implicit val jObjectReader: FromJSON[JObject] = new FromJSON[JObject] { - def read(jval: JValue): JValidation[JObject] = jval match { - case o: JObject => Valid(o) - case _ => fail("JSON object expected") - } - } - - private val validUnit = Valid(()) - - implicit val unitReader: FromJSON[Unit] = new FromJSON[Unit] { - def read(jval: JValue): JValidation[Unit] = jval match { - case JNothing | JNull | JObject(Nil) => validUnit - case _ => fail("Unexpected JSON") - } - } - - private def jsonStringReader[T](errorMessageTemplate: String)( - fromString: String => T): FromJSON[T] = - new FromJSON[T] { - def read(jval: JValue): JValidation[T] = jval match { - case JString(s) => - try Valid(fromString(s)) - catch { - case NonFatal(_) => fail(errorMessageTemplate.format(s)) - } - case _ => fail("JSON string expected.") - } - } - - // Joda Time - implicit val dateTimeReader: FromJSON[DateTime] = { - val UTCDateTimeComponents = raw"(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\.(\d{3})Z".r - - jsonStringReader("Failed to parse date/time: %s") { - case UTCDateTimeComponents(year, month, days, hours, minutes, seconds, millis) => - new DateTime( - year.toInt, - month.toInt, - days.toInt, - hours.toInt, - minutes.toInt, - seconds.toInt, - millis.toInt, - DateTimeZone.UTC) - case otherwise => - new DateTime(otherwise, DateTimeZone.UTC) - } - } - - implicit val timeReader: FromJSON[LocalTime] = jsonStringReader("Failed to parse time: %s") { - ISODateTimeFormat.localTimeParser.parseDateTime(_).toLocalTime - } - - implicit val dateReader: FromJSON[LocalDate] = jsonStringReader("Failed to parse date: %s") { - ISODateTimeFormat.localDateParser.parseDateTime(_).toLocalDate - } - - implicit val yearMonthReader: FromJSON[YearMonth] = - jsonStringReader("Failed to parse year/month: %s") { - new YearMonth(_) - } - - // java.time - // this formatter is used to parse instant in an extra lenient way - // similar to what the joda `DateTime` constructor accepts - // the accepted grammar for joda is described here: https://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTimeParser-- - // this only supports the part where the date is specified - private val lenientInstantParser = - new time.format.DateTimeFormatterBuilder() - .appendPattern("uuuu[-MM[-dd]]") - .optionalStart() - .appendPattern("'T'[HH[:mm[:ss]]]") - .appendFraction(time.temporal.ChronoField.NANO_OF_SECOND, 0, 9, true) - .optionalStart() - .appendOffset("+HH:MM", "Z") - .optionalEnd() - .optionalStart() - .appendOffset("+HHmm", "Z") - .optionalEnd() - .parseDefaulting(time.temporal.ChronoField.MONTH_OF_YEAR, 1L) - .parseDefaulting(time.temporal.ChronoField.DAY_OF_MONTH, 1L) - .parseDefaulting(time.temporal.ChronoField.HOUR_OF_DAY, 0L) - .parseDefaulting(time.temporal.ChronoField.MINUTE_OF_HOUR, 0L) - .parseDefaulting(time.temporal.ChronoField.SECOND_OF_MINUTE, 0L) - .parseDefaulting(time.temporal.ChronoField.NANO_OF_SECOND, 0L) - .parseDefaulting(time.temporal.ChronoField.OFFSET_SECONDS, 0L) - .toFormatter() - - private val lenientLocalDateParser = - new time.format.DateTimeFormatterBuilder() - .optionalStart() - .appendLiteral('+') - .optionalEnd() - .appendValue(time.temporal.ChronoField.YEAR, 1, 9, java.time.format.SignStyle.NORMAL) - .optionalStart() - .appendLiteral('-') - .appendValue(time.temporal.ChronoField.MONTH_OF_YEAR, 1, 2, java.time.format.SignStyle.NORMAL) - .optionalStart() - .appendLiteral('-') - .appendValue(time.temporal.ChronoField.DAY_OF_MONTH, 1, 2, java.time.format.SignStyle.NORMAL) - .optionalEnd() - .optionalEnd() - .parseDefaulting(time.temporal.ChronoField.MONTH_OF_YEAR, 1L) - .parseDefaulting(time.temporal.ChronoField.DAY_OF_MONTH, 1L) - .toFormatter() - - implicit val javaInstantReader: FromJSON[time.Instant] = - jsonStringReader("Failed to parse date/time: %s")(s => - time.Instant.from(lenientInstantParser.parse(s))) - - implicit val javaLocalTimeReader: FromJSON[time.LocalTime] = - jsonStringReader("Failed to parse time: %s")( - time.LocalTime.parse(_, time.format.DateTimeFormatter.ISO_LOCAL_TIME)) - - implicit val javaLocalDateReader: FromJSON[time.LocalDate] = - jsonStringReader("Failed to parse date: %s")(s => - time.LocalDate.from(lenientLocalDateParser.parse(s))) - - implicit val javaYearMonthReader: FromJSON[time.YearMonth] = - jsonStringReader("Failed to parse year/month: %s")( - time.YearMonth.parse(_, JavaYearMonthFormatter)) - - implicit val uuidReader: FromJSON[UUID] = jsonStringReader("Invalid UUID: '%s'")(UUID.fromString) - - implicit val localeReader: FromJSON[Locale] = new FromJSON[Locale] { - def read(jval: JValue): JValidation[Locale] = jval match { - case JString(s) => - s match { - case LangTag(langTag) => Valid(langTag) - case _ => fail(LangTag.invalidLangTagMessage(s)) - } - case _ => fail("JSON string expected.") - } - } } diff --git a/json/json-core/src/main/scala-2/io/sphere/json/JSON.scala b/json/json-core/src/main/scala-2/io/sphere/json/JSON.scala index 3e2366a2..7e22d74c 100644 --- a/json/json-core/src/main/scala-2/io/sphere/json/JSON.scala +++ b/json/json-core/src/main/scala-2/io/sphere/json/JSON.scala @@ -7,7 +7,7 @@ import scala.annotation.implicitNotFound @implicitNotFound("Could not find an instance of JSON for ${A}") trait JSON[A] extends FromJSON[A] with ToJSON[A] -object JSON extends JSONInstances with JSONLowPriorityImplicits { +object JSON extends JSONCatsInstances with JSONLowPriorityImplicits { @inline def apply[A](implicit instance: JSON[A]): JSON[A] = instance } diff --git a/json/json-core/src/main/scala-2/io/sphere/json/ToJSON.scala b/json/json-core/src/main/scala-2/io/sphere/json/ToJSON.scala index 8cf778ea..625ccc20 100644 --- a/json/json-core/src/main/scala-2/io/sphere/json/ToJSON.scala +++ b/json/json-core/src/main/scala-2/io/sphere/json/ToJSON.scala @@ -23,7 +23,7 @@ trait ToJSON[@specialized A] extends Serializable { class JSONWriteException(msg: String) extends JSONException(msg) -object ToJSON extends ToJSONInstances { +object ToJSON extends ToJSONCatsInstances { private val emptyJArray = JArray(Nil) private val emptyJObject = JObject(Nil) diff --git a/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala index c8a58725..a8b653bc 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala @@ -1,28 +1,7 @@ package io.sphere.json -import scala.util.control.NonFatal -import scala.collection.mutable.ListBuffer -import java.util.{Currency, Locale, UUID} -import cats.data.{NonEmptyList, Validated} -import cats.data.Validated.{Invalid, Valid} -import cats.syntax.apply.* -import cats.syntax.traverse.* -import io.sphere.json.field -import io.sphere.json.generic.{AnnotationReader, CaseClassMetaData, Field, TraitMetaData} -import io.sphere.util.{BaseMoney, HighPrecisionMoney, LangTag, Money} -import org.json4s.JsonAST.* -import org.json4s.DefaultReaders.StringReader -import org.json4s.{jvalue2monadic, jvalue2readerSyntax} -import org.joda.time.format.ISODateTimeFormat - -import java.time -import org.joda.time.DateTime -import org.joda.time.DateTimeZone -import org.joda.time.YearMonth -import org.joda.time.LocalTime -import org.joda.time.LocalDate - -import scala.deriving.Mirror +import io.sphere.json.JValidation +import org.json4s.JsonAST.JValue /** Type class for types that can be read from JSON. */ trait FromJSON[A] extends Serializable { @@ -33,519 +12,11 @@ trait FromJSON[A] extends Serializable { val fields: Set[String] = FromJSON.emptyFieldsSet } -object FromJSON extends FromJSONInstances { +object FromJSON extends FromJSONInstances with FromJSONCatsInstances with generic.DeriveJSON { inline def apply[A: JSON]: FromJSON[A] = summon[FromJSON[A]] - inline given derived[A](using Mirror.Of[A]): FromJSON[A] = Derivation.derived[A] - - private def addField(jObject: JObject, field: Field, jValue: JValue): JValue = - jValue match { - case o: JObject => - if (field.embedded) JObject(jObject.obj ++ o.obj) - else JObject(jObject.obj :+ (field.fieldName -> o)) - case other => JObject(jObject.obj :+ (field.fieldName -> other)) - } - - private object Derivation { - - import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} - - inline def derived[A](using m: Mirror.Of[A]): FromJSON[A] = - inline m match { - case s: Mirror.SumOf[A] => deriveTrait(s) - case p: Mirror.ProductOf[A] => deriveCaseClass(p) - } - - inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): FromJSON[A] = - new FromJSON[A] { - private val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] - private val typeHintMap: Map[String, String] = traitMetaData.subtypes.collect { - case (name, classMeta) if classMeta.typeHint.isDefined => - name -> classMeta.typeHint.get - } - private val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) - private val fromJsons: Seq[FromJSON[Any]] = summonFromJsons[mirrorOfSum.MirroredElemTypes] - private val names: Seq[String] = - constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector - .asInstanceOf[Vector[String]] - private val jsonsByNames: Map[String, FromJSON[Any]] = names.zip(fromJsons).toMap - - override def read(jValue: JValue): JValidation[A] = - jValue match { - case jObject: JObject => - val typeName = (jObject \ traitMetaData.typeDiscriminator).as[String] - val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - jsonsByNames(originalTypeName).read(jObject).map(_.asInstanceOf[A]) - case x => - Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$jValue'")) - } - } - - inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): FromJSON[A] = - new FromJSON[A] { - private val caseClassMetaData: CaseClassMetaData = AnnotationReader.readCaseClassMetaData[A] - private val fromJsons: Vector[FromJSON[Any]] = - summonFromJsons[mirrorOfProduct.MirroredElemTypes] - private val fieldsAndJsons: Vector[(Field, FromJSON[Any])] = - caseClassMetaData.fields.zip(fromJsons) - - private val fieldNames: Vector[String] = fieldsAndJsons.flatMap { (field, fromJson) => - if (field.embedded) fromJson.fields.toVector :+ field.name - else Vector(field.name) - } - - override val fields: Set[String] = fieldNames.toSet - - override def read(jValue: JValue): JValidation[A] = - jValue match { - case jObject: JObject => - for { - fieldsAsAList <- fieldsAndJsons - .map((field, fromJson) => readField(field, fromJson, jObject)) - .sequence - fieldsAsTuple = Tuple.fromArray(fieldsAsAList.toArray) - - } yield mirrorOfProduct.fromTuple( - fieldsAsTuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) - - case x => - Validated.invalidNel(JSONParseError(s"JSON object expected. $x")) - } - - private def readField( - field: Field, - fromJson: FromJSON[Any], - jObject: JObject): JValidation[Any] = - if (field.embedded) fromJson.read(jObject) - else io.sphere.json.field(field.fieldName, field.defaultArgument)(jObject)(fromJson) - - } - - inline private def summonFromJsons[T <: Tuple]: Vector[FromJSON[Any]] = - inline erasedValue[T] match { - case _: EmptyTuple => Vector.empty - case _: (t *: ts) => - summonInline[FromJSON[t]] - .asInstanceOf[FromJSON[Any]] +: summonFromJsons[ts] - } - } - private[FromJSON] val emptyFieldsSet: Set[String] = Set.empty inline def apply[A](using instance: FromJSON[A]): FromJSON[A] = instance - - private val validNone = Valid(None) - private val validNil = Valid(Nil) - private val validEmptyAnyVector: Valid[Vector[Any]] = Valid(Vector.empty) - private def validList[A]: Valid[List[A]] = validNil - private def validEmptyVector[A]: Valid[Vector[A]] = - validEmptyAnyVector.asInstanceOf[Valid[Vector[A]]] - - implicit def optionMapReader[A](implicit c: FromJSON[A]): FromJSON[Option[Map[String, A]]] = - new FromJSON[Option[Map[String, A]]] { - private val internalMapReader = mapReader[A] - - def read(jval: JValue): JValidation[Option[Map[String, A]]] = jval match { - case JNothing | JNull => validNone - case x => internalMapReader.read(x).map(Some.apply) - } - } - - given optionReader[A](using c: FromJSON[A]): FromJSON[Option[A]] = - new FromJSON[Option[A]] { - def read(jval: JValue): JValidation[Option[A]] = jval match { - case JNothing | JNull | JObject(Nil) => validNone - case JObject(s) if fields.nonEmpty && s.forall(t => !fields.contains(t._1)) => - validNone // if none of the optional fields are in the JSON - case x => c.read(x).map(Option.apply) - } - override val fields: Set[String] = c.fields - } - - implicit def listReader[A](implicit r: FromJSON[A]): FromJSON[List[A]] = - new FromJSON[List[A]] { - - def read(jval: JValue): JValidation[List[A]] = jval match { - case JArray(l) => - if (l.isEmpty) validList[A] - else { - // "imperative" style for performances - val errors = new ListBuffer[JSONError]() - val valids = new ListBuffer[A]() - var failedOnce: Boolean = false - l.foreach { jval => - r.read(jval) match { - case Valid(s) if !failedOnce => - valids += s - case Invalid(nel) => - errors ++= nel.toList - failedOnce = true - case _ => () - } - } - if (errors.isEmpty) - Valid(valids.result()) - else - Invalid(NonEmptyList.fromListUnsafe(errors.result())) - } - case _ => fail("JSON Array expected.") - } - } - - implicit def seqReader[A](implicit r: FromJSON[A]): FromJSON[Seq[A]] = - new FromJSON[Seq[A]] { - def read(jval: JValue): JValidation[Seq[A]] = listReader(r).read(jval) - } - - implicit def setReader[A](implicit r: FromJSON[A]): FromJSON[Set[A]] = - new FromJSON[Set[A]] { - def read(jval: JValue): JValidation[Set[A]] = jval match { - case JArray(l) => - if (l.isEmpty) Valid(Set.empty) - else listReader(r).read(jval).map(Set.apply(_*)) - case _ => fail("JSON Array expected.") - } - } - - implicit def vectorReader[A](implicit r: FromJSON[A]): FromJSON[Vector[A]] = - new FromJSON[Vector[A]] { - import scala.collection.immutable.VectorBuilder - - def read(jval: JValue): JValidation[Vector[A]] = jval match { - case JArray(l) => - if (l.isEmpty) validEmptyVector - else { - // "imperative" style for performances - val errors = new ListBuffer[JSONError]() - val valids = new VectorBuilder[A]() - var failedOnce: Boolean = false - l.foreach { jval => - r.read(jval) match { - case Valid(s) if !failedOnce => - valids += s - case Invalid(nel) => - errors ++= nel.toList - failedOnce = true - case _ => () - } - } - if (errors.isEmpty) - Valid(valids.result()) - else - Invalid(NonEmptyList.fromListUnsafe(errors.result())) - } - case _ => fail("JSON Array expected.") - } - } - - implicit def nonEmptyListReader[A](implicit r: FromJSON[A]): FromJSON[NonEmptyList[A]] = - new FromJSON[NonEmptyList[A]] { - def read(jval: JValue): JValidation[NonEmptyList[A]] = - fromJValue[List[A]](jval).andThen { - case head :: tail => Valid(NonEmptyList(head, tail)) - case Nil => fail("Non-empty JSON array expected") - } - } - - implicit val intReader: FromJSON[Int] = new FromJSON[Int] { - def read(jval: JValue): JValidation[Int] = jval match { - case JInt(i) if i.isValidInt => Valid(i.toInt) - case JLong(i) if i.isValidInt => Valid(i.toInt) - case _ => fail("JSON Number in the range of an Int expected.") - } - } - - implicit val stringReader: FromJSON[String] = new FromJSON[String] { - def read(jval: JValue): JValidation[String] = jval match { - case JString(s) => Valid(s) - case _ => fail("JSON String expected.") - } - } - - implicit val bigIntReader: FromJSON[BigInt] = new FromJSON[BigInt] { - def read(jval: JValue): JValidation[BigInt] = jval match { - case JInt(i) => Valid(i) - case JLong(l) => Valid(l) - case _ => fail("JSON Number in the range of a BigInt expected.") - } - } - - implicit val shortReader: FromJSON[Short] = new FromJSON[Short] { - def read(jval: JValue): JValidation[Short] = jval match { - case JInt(i) if i.isValidShort => Valid(i.toShort) - case JLong(l) if l.isValidShort => Valid(l.toShort) - case _ => fail("JSON Number in the range of a Short expected.") - } - } - - implicit val longReader: FromJSON[Long] = new FromJSON[Long] { - def read(jval: JValue): JValidation[Long] = jval match { - case JInt(i) => Valid(i.toLong) - case JLong(l) => Valid(l) - case _ => fail("JSON Number in the range of a Long expected.") - } - } - - implicit val floatReader: FromJSON[Float] = new FromJSON[Float] { - def read(jval: JValue): JValidation[Float] = jval match { - case JDouble(d) => Valid(d.toFloat) - case _ => fail("JSON Number in the range of a Float expected.") - } - } - - implicit val doubleReader: FromJSON[Double] = new FromJSON[Double] { - def read(jval: JValue): JValidation[Double] = jval match { - case JDouble(d) => Valid(d) - case JInt(i) => Valid(i.toDouble) - case JLong(l) => Valid(l.toDouble) - case _ => fail("JSON Number in the range of a Double expected.") - } - } - - implicit val booleanReader: FromJSON[Boolean] = new FromJSON[Boolean] { - private val cachedTrue = Valid(true) - private val cachedFalse = Valid(false) - def read(jval: JValue): JValidation[Boolean] = jval match { - case JBool(b) => if (b) cachedTrue else cachedFalse - case _ => fail("JSON Boolean expected") - } - } - - implicit def mapReader[A: FromJSON]: FromJSON[Map[String, A]] = new FromJSON[Map[String, A]] { - def read(json: JValue): JValidation[Map[String, A]] = json match { - case JObject(fs) => - // Perf note: an imperative implementation does not seem faster - fs.traverse[JValidation, (String, A)] { f => - fromJValue[A](f._2).map(v => (f._1, v)) - }.map(_.toMap) - case _ => fail("JSON Object expected") - } - } - - implicit val moneyReader: FromJSON[Money] = new FromJSON[Money] { - import Money._ - - override val fields = Set(CentAmountField, CurrencyCodeField) - - def read(value: JValue): JValidation[Money] = value match { - case o: JObject => - (field[Long](CentAmountField)(o), field[Currency](CurrencyCodeField)(o)) match { - case (Valid(centAmount), Valid(currencyCode)) => - Valid(Money.fromCentAmount(centAmount, currencyCode)) - case (Invalid(e1), Invalid(e2)) => Invalid(e1.concat(e2.toList)) - case (e1 @ Invalid(_), _) => e1 - case (_, e2 @ Invalid(_)) => e2 - } - - case _ => fail("JSON object expected.") - } - } - - implicit val highPrecisionMoneyReader: FromJSON[HighPrecisionMoney] = - new FromJSON[HighPrecisionMoney] { - import HighPrecisionMoney._ - - override val fields = Set(PreciseAmountField, CurrencyCodeField, FractionDigitsField) - - def read(value: JValue): JValidation[HighPrecisionMoney] = value match { - case o: JObject => - val validatedFields = ( - field[Long](PreciseAmountField)(o), - field[Int](FractionDigitsField)(o), - field[Currency](CurrencyCodeField)(o), - field[Option[Long]](CentAmountField)(o)) - - validatedFields.tupled.andThen { - case (preciseAmount, fractionDigits, currencyCode, centAmount) => - HighPrecisionMoney - .fromPreciseAmount(preciseAmount, fractionDigits, currencyCode, centAmount) - .leftMap(_.map(JSONParseError.apply)) - } - - case _ => - fail("JSON object expected.") - } - } - - implicit val baseMoneyReader: FromJSON[BaseMoney] = new FromJSON[BaseMoney] { - def read(value: JValue): JValidation[BaseMoney] = value match { - case o: JObject => - field[Option[String]](BaseMoney.TypeField)(o).andThen { - case None => moneyReader.read(value) - case Some(Money.TypeName) => moneyReader.read(value) - case Some(HighPrecisionMoney.TypeName) => highPrecisionMoneyReader.read(value) - case Some(tpe) => - fail( - s"Unknown money type '$tpe'. Available types are: '${Money.TypeName}', '${HighPrecisionMoney.TypeName}'.") - } - - case _ => fail("JSON object expected.") - } - } - - implicit val currencyReader: FromJSON[Currency] = new FromJSON[Currency] { - val failMsg = "ISO 4217 code JSON String expected." - def failMsgFor(input: String) = s"Currency '$input' not valid as ISO 4217 code." - - private val cachedEUR = Valid(Currency.getInstance("EUR")) - private val cachedUSD = Valid(Currency.getInstance("USD")) - private val cachedGBP = Valid(Currency.getInstance("GBP")) - private val cachedJPY = Valid(Currency.getInstance("JPY")) - - def read(jval: JValue): JValidation[Currency] = jval match { - case JString(s) => - s match { - case "EUR" => cachedEUR - case "USD" => cachedUSD - case "GBP" => cachedGBP - case "JPY" => cachedJPY - case _ => - try Valid(Currency.getInstance(s)) - catch { - case _: IllegalArgumentException => fail(failMsgFor(s)) - } - } - case _ => fail(failMsg) - } - } - - implicit val jValueReader: FromJSON[JValue] = new FromJSON[JValue] { - def read(jval: JValue): JValidation[JValue] = Valid(jval) - } - - implicit val jObjectReader: FromJSON[JObject] = new FromJSON[JObject] { - def read(jval: JValue): JValidation[JObject] = jval match { - case o: JObject => Valid(o) - case _ => fail("JSON object expected") - } - } - - private val validUnit = Valid(()) - - implicit val unitReader: FromJSON[Unit] = new FromJSON[Unit] { - def read(jval: JValue): JValidation[Unit] = jval match { - case JNothing | JNull | JObject(Nil) => validUnit - case _ => fail("Unexpected JSON") - } - } - - private def jsonStringReader[T](errorMessageTemplate: String)( - fromString: String => T): FromJSON[T] = - new FromJSON[T] { - def read(jval: JValue): JValidation[T] = jval match { - case JString(s) => - try Valid(fromString(s)) - catch { - case NonFatal(_) => fail(errorMessageTemplate.format(s)) - } - case _ => fail("JSON string expected.") - } - } - - // Joda Time - implicit val dateTimeReader: FromJSON[DateTime] = { - val UTCDateTimeComponents = raw"(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\.(\d{3})Z".r - - jsonStringReader("Failed to parse date/time: %s") { - case UTCDateTimeComponents(year, month, days, hours, minutes, seconds, millis) => - new DateTime( - year.toInt, - month.toInt, - days.toInt, - hours.toInt, - minutes.toInt, - seconds.toInt, - millis.toInt, - DateTimeZone.UTC) - case otherwise => - new DateTime(otherwise, DateTimeZone.UTC) - } - } - - implicit val timeReader: FromJSON[LocalTime] = jsonStringReader("Failed to parse time: %s") { - ISODateTimeFormat.localTimeParser.parseDateTime(_).toLocalTime - } - - implicit val dateReader: FromJSON[LocalDate] = jsonStringReader("Failed to parse date: %s") { - ISODateTimeFormat.localDateParser.parseDateTime(_).toLocalDate - } - - implicit val yearMonthReader: FromJSON[YearMonth] = - jsonStringReader("Failed to parse year/month: %s") { - new YearMonth(_) - } - - // java.time - // this formatter is used to parse instant in an extra lenient way - // similar to what the joda `DateTime` constructor accepts - // the accepted grammar for joda is described here: https://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTimeParser-- - // this only supports the part where the date is specified - private val lenientInstantParser = - new time.format.DateTimeFormatterBuilder() - .appendPattern("uuuu[-MM[-dd]]") - .optionalStart() - .appendPattern("'T'[HH[:mm[:ss]]]") - .appendFraction(time.temporal.ChronoField.NANO_OF_SECOND, 0, 9, true) - .optionalStart() - .appendOffset("+HH:MM", "Z") - .optionalEnd() - .optionalStart() - .appendOffset("+HHmm", "Z") - .optionalEnd() - .parseDefaulting(time.temporal.ChronoField.MONTH_OF_YEAR, 1L) - .parseDefaulting(time.temporal.ChronoField.DAY_OF_MONTH, 1L) - .parseDefaulting(time.temporal.ChronoField.HOUR_OF_DAY, 0L) - .parseDefaulting(time.temporal.ChronoField.MINUTE_OF_HOUR, 0L) - .parseDefaulting(time.temporal.ChronoField.SECOND_OF_MINUTE, 0L) - .parseDefaulting(time.temporal.ChronoField.NANO_OF_SECOND, 0L) - .parseDefaulting(time.temporal.ChronoField.OFFSET_SECONDS, 0L) - .toFormatter() - - private val lenientLocalDateParser = - new time.format.DateTimeFormatterBuilder() - .optionalStart() - .appendLiteral('+') - .optionalEnd() - .appendValue(time.temporal.ChronoField.YEAR, 1, 9, java.time.format.SignStyle.NORMAL) - .optionalStart() - .appendLiteral('-') - .appendValue(time.temporal.ChronoField.MONTH_OF_YEAR, 1, 2, java.time.format.SignStyle.NORMAL) - .optionalStart() - .appendLiteral('-') - .appendValue(time.temporal.ChronoField.DAY_OF_MONTH, 1, 2, java.time.format.SignStyle.NORMAL) - .optionalEnd() - .optionalEnd() - .parseDefaulting(time.temporal.ChronoField.MONTH_OF_YEAR, 1L) - .parseDefaulting(time.temporal.ChronoField.DAY_OF_MONTH, 1L) - .toFormatter() - - implicit val javaInstantReader: FromJSON[time.Instant] = - jsonStringReader("Failed to parse date/time: %s")(s => - time.Instant.from(lenientInstantParser.parse(s))) - - implicit val javaLocalTimeReader: FromJSON[time.LocalTime] = - jsonStringReader("Failed to parse time: %s")( - time.LocalTime.parse(_, time.format.DateTimeFormatter.ISO_LOCAL_TIME)) - - implicit val javaLocalDateReader: FromJSON[time.LocalDate] = - jsonStringReader("Failed to parse date: %s")(s => - time.LocalDate.from(lenientLocalDateParser.parse(s))) - - implicit val javaYearMonthReader: FromJSON[time.YearMonth] = - jsonStringReader("Failed to parse year/month: %s")( - time.YearMonth.parse(_, JavaYearMonthFormatter)) - - implicit val uuidReader: FromJSON[UUID] = jsonStringReader("Invalid UUID: '%s'")(UUID.fromString) - - implicit val localeReader: FromJSON[Locale] = new FromJSON[Locale] { - def read(jval: JValue): JValidation[Locale] = jval match { - case JString(s) => - s match { - case LangTag(langTag) => Valid(langTag) - case _ => fail(LangTag.invalidLangTagMessage(s)) - } - case _ => fail("JSON string expected.") - } - } } diff --git a/json/json-core/src/main/scala-3/io.sphere.json/JSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/JSON.scala index c73f0f60..43b24691 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/JSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/JSON.scala @@ -9,7 +9,7 @@ trait JSON[A] extends FromJSON[A] with ToJSON[A] inline def deriveJSON[A](using Mirror.Of[A]): JSON[A] = JSON.derived -object JSON extends JSONInstances { +object JSON extends JSONCatsInstances { inline def apply[A: JSON]: JSON[A] = summon[JSON[A]] inline given derived[A](using fromJSON: FromJSON[A], toJSON: ToJSON[A]): JSON[A] = diff --git a/json/json-core/src/main/scala-3/io.sphere.json/ToJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/ToJSON.scala index b4f708b9..94b09aff 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/ToJSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/ToJSON.scala @@ -18,7 +18,7 @@ trait ToJSON[A] extends Serializable { class JSONWriteException(msg: String) extends JSONException(msg) -object ToJSON extends ToJSONInstances { +object ToJSON extends ToJSONCatsInstances { inline def apply[A: JSON]: ToJSON[A] = summon[ToJSON[A]] diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveJSON.scala new file mode 100644 index 00000000..f4c8f39a --- /dev/null +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveJSON.scala @@ -0,0 +1,105 @@ +package io.sphere.json.generic + +import cats.data.Validated +import cats.syntax.traverse.* +import io.sphere.json.field +import io.sphere.json.generic.{AnnotationReader, CaseClassMetaData, Field, TraitMetaData} +import org.json4s.JsonAST.* +import org.json4s.DefaultReaders.StringReader +import org.json4s.{jvalue2monadic, jvalue2readerSyntax} + +import scala.deriving.Mirror + +import io.sphere.json.FromJSON +import io.sphere.json.* + +trait DeriveJSON { + inline given derived[A](using Mirror.Of[A]): FromJSON[A] = Derived.derived[A] + + object Derived { + + import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} + + inline def derived[A](using m: Mirror.Of[A]): FromJSON[A] = + inline m match { + case s: Mirror.SumOf[A] => deriveTrait(s) + case p: Mirror.ProductOf[A] => deriveCaseClass(p) + } + + inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): FromJSON[A] = + new FromJSON[A] { + private val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] + private val typeHintMap: Map[String, String] = traitMetaData.subtypes.collect { + case (name, classMeta) if classMeta.typeHint.isDefined => + name -> classMeta.typeHint.get + } + private val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) + private val fromJsons: Seq[FromJSON[Any]] = summonFromJsons[mirrorOfSum.MirroredElemTypes] + private val names: Seq[String] = + constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector + .asInstanceOf[Vector[String]] + private val jsonsByNames: Map[String, FromJSON[Any]] = names.zip(fromJsons).toMap + + override def read(jValue: JValue): JValidation[A] = + jValue match { + case jObject: JObject => + val typeName = (jObject \ traitMetaData.typeDiscriminator).as[String] + val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) + jsonsByNames(originalTypeName).read(jObject).map(_.asInstanceOf[A]) + case x => + Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$jValue'")) + } + } + + inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): FromJSON[A] = + new FromJSON[A] { + private val caseClassMetaData: CaseClassMetaData = AnnotationReader.readCaseClassMetaData[A] + private val fromJsons: Vector[FromJSON[Any]] = + summonFromJsons[mirrorOfProduct.MirroredElemTypes] + private val fieldsAndJsons: Vector[(Field, FromJSON[Any])] = + caseClassMetaData.fields.zip(fromJsons) + + private val fieldNames: Vector[String] = fieldsAndJsons.flatMap { (field, fromJson) => + if (field.embedded) fromJson.fields.toVector :+ field.name + else Vector(field.name) + } + + override val fields: Set[String] = fieldNames.toSet + + override def read(jValue: JValue): JValidation[A] = + jValue match { + case jObject: JObject => + println(s"deriveCaseClass ${fields}") + for { + fieldsAsAList <- fieldsAndJsons + .map((field, fromJson) => readField(field, fromJson, jObject)) + .sequence + fieldsAsTuple = Tuple.fromArray(fieldsAsAList.toArray) + + } yield mirrorOfProduct.fromTuple( + fieldsAsTuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) + + case x => + Validated.invalidNel(JSONParseError(s"JSON object expected. $x")) + } + + private def readField( + field: Field, + fromJson: FromJSON[Any], + jObject: JObject): JValidation[Any] = + if (field.embedded) fromJson.read(jObject) + else io.sphere.json.field(field.fieldName, field.defaultArgument)(jObject)(fromJson) + + } + + inline private def summonFromJsons[T <: Tuple]: Vector[FromJSON[Any]] = + inline erasedValue[T] match { + case _: EmptyTuple => Vector.empty + case _: (t *: ts) => + summonInline[FromJSON[t]] + .asInstanceOf[FromJSON[Any]] +: summonFromJsons[ts] + } + + } + +} diff --git a/json/json-core/src/main/scala/io/sphere/json/FromJSONInstances.scala b/json/json-core/src/main/scala/io/sphere/json/FromJSONInstances.scala new file mode 100644 index 00000000..5537104d --- /dev/null +++ b/json/json-core/src/main/scala/io/sphere/json/FromJSONInstances.scala @@ -0,0 +1,438 @@ +package io.sphere.json + +import cats.data.NonEmptyList +import cats.data.Validated.{Invalid, Valid} +import cats.syntax.apply._ +import cats.syntax.traverse._ +import io.sphere.util.{BaseMoney, HighPrecisionMoney, LangTag, Money} +import org.json4s.JsonAST._ + +import java.time +import java.util.{Currency, Locale, UUID} +import scala.collection.mutable.ListBuffer +import scala.util.control.NonFatal +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import org.joda.time.YearMonth +import org.joda.time.LocalTime +import org.joda.time.LocalDate +import org.joda.time.format.ISODateTimeFormat + +trait FromJSONInstances { + + private val validNone = Valid(None) + private val validNil = Valid(Nil) + private val validEmptyAnyVector: Valid[Vector[Any]] = Valid(Vector.empty) + private def validList[A]: Valid[List[A]] = validNil + private def validEmptyVector[A]: Valid[Vector[A]] = + validEmptyAnyVector.asInstanceOf[Valid[Vector[A]]] + + implicit def optionMapReader[@specialized A](implicit + c: FromJSON[A]): FromJSON[Option[Map[String, A]]] = + new FromJSON[Option[Map[String, A]]] { + private val internalMapReader = mapReader[A] + + def read(jval: JValue): JValidation[Option[Map[String, A]]] = jval match { + case JNothing | JNull => validNone + case x => internalMapReader.read(x).map(Some.apply) + } + } + + implicit def optionReader[@specialized A](implicit c: FromJSON[A]): FromJSON[Option[A]] = + new FromJSON[Option[A]] { + def read(jval: JValue): JValidation[Option[A]] = jval match { + case JNothing | JNull | JObject(Nil) => validNone + case JObject(s) if fields.nonEmpty && s.forall(t => !fields.contains(t._1)) => + validNone // if none of the optional fields are in the JSON + case x => c.read(x).map(Option.apply) + } + override val fields: Set[String] = c.fields + } + + implicit def listReader[@specialized A](implicit r: FromJSON[A]): FromJSON[List[A]] = + new FromJSON[List[A]] { + + def read(jval: JValue): JValidation[List[A]] = jval match { + case JArray(l) => + if (l.isEmpty) validList[A] + else { + // "imperative" style for performances + val errors = new ListBuffer[JSONError]() + val valids = new ListBuffer[A]() + var failedOnce: Boolean = false + l.foreach { jval => + r.read(jval) match { + case Valid(s) if !failedOnce => + valids += s + case Invalid(nel) => + errors ++= nel.toList + failedOnce = true + case _ => () + } + } + if (errors.isEmpty) + Valid(valids.result()) + else + Invalid(NonEmptyList.fromListUnsafe(errors.result())) + } + case _ => fail("JSON Array expected.") + } + } + + implicit def seqReader[@specialized A](implicit r: FromJSON[A]): FromJSON[Seq[A]] = + new FromJSON[Seq[A]] { + def read(jval: JValue): JValidation[Seq[A]] = listReader(r).read(jval) + } + + implicit def setReader[@specialized A](implicit r: FromJSON[A]): FromJSON[Set[A]] = + new FromJSON[Set[A]] { + def read(jval: JValue): JValidation[Set[A]] = jval match { + case JArray(l) => + if (l.isEmpty) Valid(Set.empty) + else listReader(r).read(jval).map(Set.apply(_: _*)) + case _ => fail("JSON Array expected.") + } + } + + implicit def vectorReader[@specialized A](implicit r: FromJSON[A]): FromJSON[Vector[A]] = + new FromJSON[Vector[A]] { + import scala.collection.immutable.VectorBuilder + + def read(jval: JValue): JValidation[Vector[A]] = jval match { + case JArray(l) => + if (l.isEmpty) validEmptyVector + else { + // "imperative" style for performances + val errors = new ListBuffer[JSONError]() + val valids = new VectorBuilder[A]() + var failedOnce: Boolean = false + l.foreach { jval => + r.read(jval) match { + case Valid(s) if !failedOnce => + valids += s + case Invalid(nel) => + errors ++= nel.toList + failedOnce = true + case _ => () + } + } + if (errors.isEmpty) + Valid(valids.result()) + else + Invalid(NonEmptyList.fromListUnsafe(errors.result())) + } + case _ => fail("JSON Array expected.") + } + } + + implicit def nonEmptyListReader[A](implicit r: FromJSON[A]): FromJSON[NonEmptyList[A]] = + new FromJSON[NonEmptyList[A]] { + def read(jval: JValue): JValidation[NonEmptyList[A]] = + fromJValue[List[A]](jval).andThen { + case head :: tail => Valid(NonEmptyList(head, tail)) + case Nil => fail("Non-empty JSON array expected") + } + } + + implicit val intReader: FromJSON[Int] = new FromJSON[Int] { + def read(jval: JValue): JValidation[Int] = jval match { + case JInt(i) if i.isValidInt => Valid(i.toInt) + case JLong(i) if i.isValidInt => Valid(i.toInt) + case _ => fail("JSON Number in the range of an Int expected.") + } + } + + implicit val stringReader: FromJSON[String] = new FromJSON[String] { + def read(jval: JValue): JValidation[String] = jval match { + case JString(s) => Valid(s) + case _ => fail("JSON String expected.") + } + } + + implicit val bigIntReader: FromJSON[BigInt] = new FromJSON[BigInt] { + def read(jval: JValue): JValidation[BigInt] = jval match { + case JInt(i) => Valid(i) + case JLong(l) => Valid(l) + case _ => fail("JSON Number in the range of a BigInt expected.") + } + } + + implicit val shortReader: FromJSON[Short] = new FromJSON[Short] { + def read(jval: JValue): JValidation[Short] = jval match { + case JInt(i) if i.isValidShort => Valid(i.toShort) + case JLong(l) if l.isValidShort => Valid(l.toShort) + case _ => fail("JSON Number in the range of a Short expected.") + } + } + + implicit val longReader: FromJSON[Long] = new FromJSON[Long] { + def read(jval: JValue): JValidation[Long] = jval match { + case JInt(i) => Valid(i.toLong) + case JLong(l) => Valid(l) + case _ => fail("JSON Number in the range of a Long expected.") + } + } + + implicit val floatReader: FromJSON[Float] = new FromJSON[Float] { + def read(jval: JValue): JValidation[Float] = jval match { + case JDouble(d) => Valid(d.toFloat) + case _ => fail("JSON Number in the range of a Float expected.") + } + } + + implicit val doubleReader: FromJSON[Double] = new FromJSON[Double] { + def read(jval: JValue): JValidation[Double] = jval match { + case JDouble(d) => Valid(d) + case JInt(i) => Valid(i.toDouble) + case JLong(l) => Valid(l.toDouble) + case _ => fail("JSON Number in the range of a Double expected.") + } + } + + implicit val booleanReader: FromJSON[Boolean] = new FromJSON[Boolean] { + private val cachedTrue = Valid(true) + private val cachedFalse = Valid(false) + def read(jval: JValue): JValidation[Boolean] = jval match { + case JBool(b) => if (b) cachedTrue else cachedFalse + case _ => fail("JSON Boolean expected") + } + } + + implicit def mapReader[A: FromJSON]: FromJSON[Map[String, A]] = new FromJSON[Map[String, A]] { + def read(json: JValue): JValidation[Map[String, A]] = json match { + case JObject(fs) => + // Perf note: an imperative implementation does not seem faster + fs.traverse[JValidation, (String, A)] { f => + fromJValue[A](f._2).map(v => (f._1, v)) + }.map(_.toMap) + case _ => fail("JSON Object expected") + } + } + + implicit val moneyReader: FromJSON[Money] = new FromJSON[Money] { + import Money._ + + override val fields = Set(CentAmountField, CurrencyCodeField) + + def read(value: JValue): JValidation[Money] = value match { + case o: JObject => + (field[Long](CentAmountField)(o), field[Currency](CurrencyCodeField)(o)) match { + case (Valid(centAmount), Valid(currencyCode)) => + Valid(Money.fromCentAmount(centAmount, currencyCode)) + case (Invalid(e1), Invalid(e2)) => Invalid(e1.concat(e2.toList)) + case (e1 @ Invalid(_), _) => e1 + case (_, e2 @ Invalid(_)) => e2 + } + + case _ => fail("JSON object expected.") + } + } + + implicit val highPrecisionMoneyReader: FromJSON[HighPrecisionMoney] = + new FromJSON[HighPrecisionMoney] { + import HighPrecisionMoney._ + + override val fields = Set(PreciseAmountField, CurrencyCodeField, FractionDigitsField) + + def read(value: JValue): JValidation[HighPrecisionMoney] = value match { + case o: JObject => + val validatedFields = ( + field[Long](PreciseAmountField)(o), + field[Int](FractionDigitsField)(o), + field[Currency](CurrencyCodeField)(o), + field[Option[Long]](CentAmountField)(o)) + + validatedFields.tupled.andThen { + case (preciseAmount, fractionDigits, currencyCode, centAmount) => + HighPrecisionMoney + .fromPreciseAmount(preciseAmount, fractionDigits, currencyCode, centAmount) + .leftMap(_.map(JSONParseError.apply)) + } + + case _ => + fail("JSON object expected.") + } + } + + implicit val baseMoneyReader: FromJSON[BaseMoney] = new FromJSON[BaseMoney] { + def read(value: JValue): JValidation[BaseMoney] = value match { + case o: JObject => + field[Option[String]](BaseMoney.TypeField)(o).andThen { + case None => moneyReader.read(value) + case Some(Money.TypeName) => moneyReader.read(value) + case Some(HighPrecisionMoney.TypeName) => highPrecisionMoneyReader.read(value) + case Some(tpe) => + fail( + s"Unknown money type '$tpe'. Available types are: '${Money.TypeName}', '${HighPrecisionMoney.TypeName}'.") + } + + case _ => fail("JSON object expected.") + } + } + + implicit val currencyReader: FromJSON[Currency] = new FromJSON[Currency] { + val failMsg = "ISO 4217 code JSON String expected." + def failMsgFor(input: String) = s"Currency '$input' not valid as ISO 4217 code." + + private val cachedEUR = Valid(Currency.getInstance("EUR")) + private val cachedUSD = Valid(Currency.getInstance("USD")) + private val cachedGBP = Valid(Currency.getInstance("GBP")) + private val cachedJPY = Valid(Currency.getInstance("JPY")) + + def read(jval: JValue): JValidation[Currency] = jval match { + case JString(s) => + s match { + case "EUR" => cachedEUR + case "USD" => cachedUSD + case "GBP" => cachedGBP + case "JPY" => cachedJPY + case _ => + try Valid(Currency.getInstance(s)) + catch { + case _: IllegalArgumentException => fail(failMsgFor(s)) + } + } + case _ => fail(failMsg) + } + } + + implicit val jValueReader: FromJSON[JValue] = new FromJSON[JValue] { + def read(jval: JValue): JValidation[JValue] = Valid(jval) + } + + implicit val jObjectReader: FromJSON[JObject] = new FromJSON[JObject] { + def read(jval: JValue): JValidation[JObject] = jval match { + case o: JObject => Valid(o) + case _ => fail("JSON object expected") + } + } + + private val validUnit = Valid(()) + + implicit val unitReader: FromJSON[Unit] = new FromJSON[Unit] { + def read(jval: JValue): JValidation[Unit] = jval match { + case JNothing | JNull | JObject(Nil) => validUnit + case _ => fail("Unexpected JSON") + } + } + + private def jsonStringReader[T](errorMessageTemplate: String)( + fromString: String => T): FromJSON[T] = + new FromJSON[T] { + def read(jval: JValue): JValidation[T] = jval match { + case JString(s) => + try Valid(fromString(s)) + catch { + case NonFatal(_) => fail(errorMessageTemplate.format(s)) + } + case _ => fail("JSON string expected.") + } + } + + // Joda Time + implicit val dateTimeReader: FromJSON[DateTime] = { + val UTCDateTimeComponents = raw"(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\.(\d{3})Z".r + + jsonStringReader("Failed to parse date/time: %s") { + case UTCDateTimeComponents(year, month, days, hours, minutes, seconds, millis) => + new DateTime( + year.toInt, + month.toInt, + days.toInt, + hours.toInt, + minutes.toInt, + seconds.toInt, + millis.toInt, + DateTimeZone.UTC) + case otherwise => + new DateTime(otherwise, DateTimeZone.UTC) + } + } + + implicit val timeReader: FromJSON[LocalTime] = jsonStringReader("Failed to parse time: %s") { + ISODateTimeFormat.localTimeParser.parseDateTime(_).toLocalTime + } + + implicit val dateReader: FromJSON[LocalDate] = jsonStringReader("Failed to parse date: %s") { + ISODateTimeFormat.localDateParser.parseDateTime(_).toLocalDate + } + + implicit val yearMonthReader: FromJSON[YearMonth] = + jsonStringReader("Failed to parse year/month: %s") { + new YearMonth(_) + } + + // java.time + // this formatter is used to parse instant in an extra lenient way + // similar to what the joda `DateTime` constructor accepts + // the accepted grammar for joda is described here: https://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTimeParser-- + // this only supports the part where the date is specified + private val lenientInstantParser = + new time.format.DateTimeFormatterBuilder() + .appendPattern("uuuu[-MM[-dd]]") + .optionalStart() + .appendPattern("'T'[HH[:mm[:ss]]]") + .appendFraction(time.temporal.ChronoField.NANO_OF_SECOND, 0, 9, true) + .optionalStart() + .appendOffset("+HH:MM", "Z") + .optionalEnd() + .optionalStart() + .appendOffset("+HHmm", "Z") + .optionalEnd() + .parseDefaulting(time.temporal.ChronoField.MONTH_OF_YEAR, 1L) + .parseDefaulting(time.temporal.ChronoField.DAY_OF_MONTH, 1L) + .parseDefaulting(time.temporal.ChronoField.HOUR_OF_DAY, 0L) + .parseDefaulting(time.temporal.ChronoField.MINUTE_OF_HOUR, 0L) + .parseDefaulting(time.temporal.ChronoField.SECOND_OF_MINUTE, 0L) + .parseDefaulting(time.temporal.ChronoField.NANO_OF_SECOND, 0L) + .parseDefaulting(time.temporal.ChronoField.OFFSET_SECONDS, 0L) + .toFormatter() + + private val lenientLocalDateParser = + new time.format.DateTimeFormatterBuilder() + .optionalStart() + .appendLiteral('+') + .optionalEnd() + .appendValue(time.temporal.ChronoField.YEAR, 1, 9, java.time.format.SignStyle.NORMAL) + .optionalStart() + .appendLiteral('-') + .appendValue(time.temporal.ChronoField.MONTH_OF_YEAR, 1, 2, java.time.format.SignStyle.NORMAL) + .optionalStart() + .appendLiteral('-') + .appendValue(time.temporal.ChronoField.DAY_OF_MONTH, 1, 2, java.time.format.SignStyle.NORMAL) + .optionalEnd() + .optionalEnd() + .parseDefaulting(time.temporal.ChronoField.MONTH_OF_YEAR, 1L) + .parseDefaulting(time.temporal.ChronoField.DAY_OF_MONTH, 1L) + .toFormatter() + + implicit val javaInstantReader: FromJSON[time.Instant] = + jsonStringReader("Failed to parse date/time: %s")(s => + time.Instant.from(lenientInstantParser.parse(s))) + + implicit val javaLocalTimeReader: FromJSON[time.LocalTime] = + jsonStringReader("Failed to parse time: %s")( + time.LocalTime.parse(_, time.format.DateTimeFormatter.ISO_LOCAL_TIME)) + + implicit val javaLocalDateReader: FromJSON[time.LocalDate] = + jsonStringReader("Failed to parse date: %s")(s => + time.LocalDate.from(lenientLocalDateParser.parse(s))) + + implicit val javaYearMonthReader: FromJSON[time.YearMonth] = + jsonStringReader("Failed to parse year/month: %s")( + time.YearMonth.parse(_, JavaYearMonthFormatter)) + + implicit val uuidReader: FromJSON[UUID] = jsonStringReader("Invalid UUID: '%s'")(UUID.fromString) + + implicit val localeReader: FromJSON[Locale] = new FromJSON[Locale] { + def read(jval: JValue): JValidation[Locale] = jval match { + case JString(s) => + s match { + case LangTag(langTag) => Valid(langTag) + case _ => fail(LangTag.invalidLangTagMessage(s)) + } + case _ => fail("JSON string expected.") + } + } + +} diff --git a/json/json-core/src/main/scala/io/sphere/json/catsinstances/package.scala b/json/json-core/src/main/scala/io/sphere/json/catsinstances/package.scala index 779fd45d..0d562785 100644 --- a/json/json-core/src/main/scala/io/sphere/json/catsinstances/package.scala +++ b/json/json-core/src/main/scala/io/sphere/json/catsinstances/package.scala @@ -5,17 +5,20 @@ import org.json4s.JValue /** Cats instances for [[JSON]], [[FromJSON]] and [[ToJSON]] */ -package object catsinstances extends JSONInstances with FromJSONInstances with ToJSONInstances +package object catsinstances + extends JSONCatsInstances + with FromJSONCatsInstances + with ToJSONCatsInstances -trait JSONInstances { +trait JSONCatsInstances { implicit val catsInvariantForJSON: Invariant[JSON] = new JSONInvariant } -trait FromJSONInstances { +trait FromJSONCatsInstances { implicit val catsFunctorForFromJSON: Functor[FromJSON] = new FromJSONFunctor } -trait ToJSONInstances { +trait ToJSONCatsInstances { implicit val catsContravariantForToJSON: Contravariant[ToJSON] = new ToJSONContravariant } From 54be0ab652d6177e861f00bc51ae30be851b590f Mon Sep 17 00:00:00 2001 From: Marcelo Gomes Date: Thu, 24 Apr 2025 18:18:22 +0200 Subject: [PATCH 080/142] Make the code work in the editor --- build.sbt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.sbt b/build.sbt index 81d44216..b1c26441 100644 --- a/build.sbt +++ b/build.sbt @@ -118,18 +118,21 @@ lazy val `sphere-libs` = project lazy val `sphere-util` = project .in(file("./util")) .settings(standardSettings: _*) + .settings(scalaVersion := scala3) .settings(crossScalaVersions := Seq(scala212, scala213, scala3)) .settings(homepage := Some(url("https://github.com/commercetools/sphere-scala-libs/README.md"))) lazy val `sphere-json-core` = project .in(file("./json/json-core")) .settings(standardSettings: _*) + .settings(scalaVersion := scala3) .settings(crossScalaVersions := Seq(scala212, scala213, scala3)) .dependsOn(`sphere-util`) lazy val `sphere-mongo-core` = project .in(file("./mongo/mongo-core")) .settings(standardSettings: _*) + .settings(scalaVersion := scala3) .settings(crossScalaVersions := Seq(scala212, scala213, scala3)) .dependsOn(`sphere-util`) From c00b484e6f16b57755f5d43a4777ec3a6f095df5 Mon Sep 17 00:00:00 2001 From: Marcelo Gomes Date: Thu, 24 Apr 2025 18:18:42 +0200 Subject: [PATCH 081/142] Fix "New anonymous class definition will be duplicated at each inline sites" --- json/json-core/src/main/scala-3/io.sphere.json/JSON.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/json/json-core/src/main/scala-3/io.sphere.json/JSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/JSON.scala index 43b24691..201b1d85 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/JSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/JSON.scala @@ -12,7 +12,9 @@ inline def deriveJSON[A](using Mirror.Of[A]): JSON[A] = JSON.derived object JSON extends JSONCatsInstances { inline def apply[A: JSON]: JSON[A] = summon[JSON[A]] - inline given derived[A](using fromJSON: FromJSON[A], toJSON: ToJSON[A]): JSON[A] = + inline given derived[A](using fromJSON: FromJSON[A], toJSON: ToJSON[A]): JSON[A] = instance + + private def instance[A](using fromJSON: FromJSON[A], toJSON: ToJSON[A]): JSON[A] = new JSON[A] { override def read(jval: JValue): JValidation[A] = fromJSON.read(jval) @@ -20,7 +22,6 @@ object JSON extends JSONCatsInstances { override val fields: Set[String] = fromJSON.fields } - } class JSONException(msg: String) extends RuntimeException(msg) From 1756aadd55b638f9d9793da43963022d37c37b27 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 25 Apr 2025 11:25:02 +0200 Subject: [PATCH 082/142] Move ToJSON instances to a common file (between scala 2/3) --- .../main/scala-2/io/sphere/json/ToJSON.scala | 208 +------------ .../scala-3/io.sphere.json/FromJSON.scala | 2 +- .../main/scala-3/io.sphere.json/ToJSON.scala | 277 +----------------- ...{DeriveJSON.scala => DeriveFromJSON.scala} | 6 +- .../io.sphere.json/generic/DeriveToJSON.scala | 80 +++++ .../io/sphere/json/ToJSONInstances.scala | 214 ++++++++++++++ 6 files changed, 303 insertions(+), 484 deletions(-) rename json/json-core/src/main/scala-3/io.sphere.json/generic/{DeriveJSON.scala => DeriveFromJSON.scala} (98%) create mode 100644 json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala create mode 100644 json/json-core/src/main/scala/io/sphere/json/ToJSONInstances.scala diff --git a/json/json-core/src/main/scala-2/io/sphere/json/ToJSON.scala b/json/json-core/src/main/scala-2/io/sphere/json/ToJSON.scala index 625ccc20..bd02e1b7 100644 --- a/json/json-core/src/main/scala-2/io/sphere/json/ToJSON.scala +++ b/json/json-core/src/main/scala-2/io/sphere/json/ToJSON.scala @@ -1,19 +1,8 @@ package io.sphere.json -import cats.data.NonEmptyList -import java.util.{Currency, Locale, UUID} - -import io.sphere.util.{BaseMoney, HighPrecisionMoney, Money} -import org.json4s.JsonAST._ -import org.joda.time.DateTime -import org.joda.time.DateTimeZone -import org.joda.time.LocalTime -import org.joda.time.LocalDate -import org.joda.time.YearMonth -import org.joda.time.format.ISODateTimeFormat - import scala.annotation.implicitNotFound import java.time +import org.json4s.JsonAST.JValue /** Type class for types that can be written to JSON. */ @implicitNotFound("Could not find an instance of ToJSON for ${A}") @@ -23,10 +12,7 @@ trait ToJSON[@specialized A] extends Serializable { class JSONWriteException(msg: String) extends JSONException(msg) -object ToJSON extends ToJSONCatsInstances { - - private val emptyJArray = JArray(Nil) - private val emptyJObject = JObject(Nil) +object ToJSON extends ToJSONInstances with ToJSONCatsInstances { @inline def apply[A](implicit instance: ToJSON[A]): ToJSON[A] = instance @@ -36,194 +22,4 @@ object ToJSON extends ToJSONCatsInstances { override def write(value: T): JValue = toJson(value) } - implicit def optionWriter[@specialized A](implicit c: ToJSON[A]): ToJSON[Option[A]] = - new ToJSON[Option[A]] { - def write(opt: Option[A]): JValue = opt match { - case Some(a) => c.write(a) - case None => JNothing - } - } - - implicit def listWriter[@specialized A](implicit w: ToJSON[A]): ToJSON[List[A]] = - new ToJSON[List[A]] { - def write(l: List[A]): JValue = - if (l.isEmpty) emptyJArray - else JArray(l.map(w.write)) - } - - implicit def nonEmptyListWriter[A](implicit w: ToJSON[A]): ToJSON[NonEmptyList[A]] = - new ToJSON[NonEmptyList[A]] { - def write(l: NonEmptyList[A]): JValue = JArray(l.toList.map(w.write)) - } - - implicit def seqWriter[@specialized A](implicit w: ToJSON[A]): ToJSON[Seq[A]] = - new ToJSON[Seq[A]] { - def write(s: Seq[A]): JValue = - if (s.isEmpty) emptyJArray - else JArray(s.iterator.map(w.write).toList) - } - - implicit def setWriter[@specialized A](implicit w: ToJSON[A]): ToJSON[Set[A]] = - new ToJSON[Set[A]] { - def write(s: Set[A]): JValue = - if (s.isEmpty) emptyJArray - else JArray(s.iterator.map(w.write).toList) - } - - implicit def vectorWriter[@specialized A](implicit w: ToJSON[A]): ToJSON[Vector[A]] = - new ToJSON[Vector[A]] { - def write(v: Vector[A]): JValue = - if (v.isEmpty) emptyJArray - else JArray(v.iterator.map(w.write).toList) - } - - implicit val intWriter: ToJSON[Int] = new ToJSON[Int] { - def write(i: Int): JValue = JLong(i) - } - - implicit val stringWriter: ToJSON[String] = new ToJSON[String] { - def write(s: String): JValue = JString(s) - } - - implicit val bigIntWriter: ToJSON[BigInt] = new ToJSON[BigInt] { - def write(i: BigInt): JValue = JInt(i) - } - - implicit val shortWriter: ToJSON[Short] = new ToJSON[Short] { - def write(s: Short): JValue = JLong(s) - } - - implicit val longWriter: ToJSON[Long] = new ToJSON[Long] { - def write(l: Long): JValue = JLong(l) - } - - implicit val floatWriter: ToJSON[Float] = new ToJSON[Float] { - def write(f: Float): JValue = JDouble(f) - } - - implicit val doubleWriter: ToJSON[Double] = new ToJSON[Double] { - def write(d: Double): JValue = JDouble(d) - } - - implicit val booleanWriter: ToJSON[Boolean] = new ToJSON[Boolean] { - def write(b: Boolean): JValue = if (b) JBool.True else JBool.False - } - - implicit def mapWriter[A: ToJSON]: ToJSON[Map[String, A]] = new ToJSON[Map[String, A]] { - def write(m: Map[String, A]) = - if (m.isEmpty) emptyJObject - else - JObject(m.iterator.map { case (k, v) => - JField(k, toJValue(v)) - }.toList) - } - - implicit val moneyWriter: ToJSON[Money] = new ToJSON[Money] { - import Money._ - - def write(m: Money): JValue = JObject( - JField(BaseMoney.TypeField, toJValue(m.`type`)) :: - JField(CurrencyCodeField, toJValue(m.currency)) :: - JField(CentAmountField, toJValue(m.centAmount)) :: - JField(FractionDigitsField, toJValue(m.currency.getDefaultFractionDigits)) :: - Nil - ) - } - - implicit val highPrecisionMoneyWriter: ToJSON[HighPrecisionMoney] = - new ToJSON[HighPrecisionMoney] { - import HighPrecisionMoney._ - def write(m: HighPrecisionMoney): JValue = JObject( - JField(BaseMoney.TypeField, toJValue(m.`type`)) :: - JField(CurrencyCodeField, toJValue(m.currency)) :: - JField(CentAmountField, toJValue(m.centAmount)) :: - JField(PreciseAmountField, toJValue(m.preciseAmount)) :: - JField(FractionDigitsField, toJValue(m.fractionDigits)) :: - Nil - ) - } - - implicit val baseMoneyWriter: ToJSON[BaseMoney] = new ToJSON[BaseMoney] { - def write(m: BaseMoney): JValue = m match { - case m: Money => moneyWriter.write(m) - case m: HighPrecisionMoney => highPrecisionMoneyWriter.write(m) - } - } - - implicit val currencyWriter: ToJSON[Currency] = new ToJSON[Currency] { - def write(c: Currency): JValue = toJValue(c.getCurrencyCode) - } - - implicit val jValueWriter: ToJSON[JValue] = new ToJSON[JValue] { - def write(jval: JValue): JValue = jval - } - - implicit val jObjectWriter: ToJSON[JObject] = new ToJSON[JObject] { - def write(jObj: JObject): JValue = jObj - } - - implicit val unitWriter: ToJSON[Unit] = new ToJSON[Unit] { - def write(u: Unit): JValue = JNothing - } - - // Joda time - implicit val dateTimeWriter: ToJSON[DateTime] = new ToJSON[DateTime] { - def write(dt: DateTime): JValue = JString( - ISODateTimeFormat.dateTime.print(dt.withZone(DateTimeZone.UTC))) - } - - implicit val timeWriter: ToJSON[LocalTime] = new ToJSON[LocalTime] { - def write(lt: LocalTime): JValue = JString(ISODateTimeFormat.time.print(lt)) - } - - implicit val dateWriter: ToJSON[LocalDate] = new ToJSON[LocalDate] { - def write(ld: LocalDate): JValue = JString(ISODateTimeFormat.date.print(ld)) - } - - implicit val yearMonthWriter: ToJSON[YearMonth] = new ToJSON[YearMonth] { - def write(ym: YearMonth): JValue = JString(ISODateTimeFormat.yearMonth().print(ym)) - } - - // java.time - - // always format the milliseconds - private val javaInstantFormatter = new time.format.DateTimeFormatterBuilder() - .appendInstant(3) - .toFormatter() - implicit val javaInstantWriter: ToJSON[time.Instant] = new ToJSON[time.Instant] { - def write(value: time.Instant): JValue = JString( - javaInstantFormatter.format(time.OffsetDateTime.ofInstant(value, time.ZoneOffset.UTC))) - } - - // always format the milliseconds - private val javaLocalTimeFormatter = new time.format.DateTimeFormatterBuilder() - .appendPattern("HH:mm:ss.SSS") - .toFormatter() - implicit val javaTimeWriter: ToJSON[time.LocalTime] = new ToJSON[time.LocalTime] { - def write(value: time.LocalTime): JValue = JString(javaLocalTimeFormatter.format(value)) - } - - implicit val javaDateWriter: ToJSON[time.LocalDate] = new ToJSON[time.LocalDate] { - def write(value: time.LocalDate): JValue = JString( - time.format.DateTimeFormatter.ISO_LOCAL_DATE.format(value)) - } - - implicit val javaYearMonth: ToJSON[time.YearMonth] = new ToJSON[time.YearMonth] { - def write(value: time.YearMonth): JValue = JString(JavaYearMonthFormatter.format(value)) - } - - implicit val uuidWriter: ToJSON[UUID] = new ToJSON[UUID] { - def write(uuid: UUID): JValue = JString(uuid.toString) - } - - implicit val localeWriter: ToJSON[Locale] = new ToJSON[Locale] { - def write(locale: Locale): JValue = JString(locale.toLanguageTag) - } - - implicit def eitherWriter[A: ToJSON, B: ToJSON]: ToJSON[Either[A, B]] = new ToJSON[Either[A, B]] { - def write(e: Either[A, B]): JValue = e match { - case Left(l) => toJValue(l) - case Right(r) => toJValue(r) - } - } } diff --git a/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala index a8b653bc..820fc31b 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala @@ -12,7 +12,7 @@ trait FromJSON[A] extends Serializable { val fields: Set[String] = FromJSON.emptyFieldsSet } -object FromJSON extends FromJSONInstances with FromJSONCatsInstances with generic.DeriveJSON { +object FromJSON extends FromJSONInstances with FromJSONCatsInstances with generic.DeriveFromJSON { inline def apply[A: JSON]: FromJSON[A] = summon[FromJSON[A]] diff --git a/json/json-core/src/main/scala-3/io.sphere.json/ToJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/ToJSON.scala index 94b09aff..1ed15304 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/ToJSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/ToJSON.scala @@ -1,15 +1,9 @@ package io.sphere.json -import cats.data.NonEmptyList -import io.sphere.json.generic.{AnnotationReader, CaseClassMetaData, Field, TraitMetaData} -import io.sphere.util.{BaseMoney, HighPrecisionMoney, Money} -import org.joda.time.* -import org.joda.time.format.ISODateTimeFormat -import org.json4s.JsonAST.* +import org.json4s.JsonAST.JValue import java.time import java.util.{Currency, Locale, UUID} -import scala.deriving.Mirror /** Type class for types that can be written to JSON. */ trait ToJSON[A] extends Serializable { @@ -18,279 +12,14 @@ trait ToJSON[A] extends Serializable { class JSONWriteException(msg: String) extends JSONException(msg) -object ToJSON extends ToJSONCatsInstances { - - inline def apply[A: JSON]: ToJSON[A] = summon[ToJSON[A]] - - inline given derived[A](using Mirror.Of[A]): ToJSON[A] = Derivation.derived[A] - - private def addField(jObject: JObject, field: Field, jValue: JValue): JValue = - jValue match { - case o: JObject => - if (field.embedded) JObject(jObject.obj ++ o.obj) - else JObject(jObject.obj :+ (field.fieldName -> o)) - case other => JObject(jObject.obj :+ (field.fieldName -> other)) - } - - private object Derivation { - - import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} - - inline def derived[A](using m: Mirror.Of[A]): ToJSON[A] = - inline m match { - case s: Mirror.SumOf[A] => deriveTrait(s) - case p: Mirror.ProductOf[A] => deriveCaseClass(p) - } - - inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): ToJSON[A] = - new ToJSON[A] { - private val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] - private val typeHintMap: Map[String, String] = traitMetaData.subtypes.collect { - case (name, classMeta) if classMeta.typeHint.isDefined => - name -> classMeta.typeHint.get - } - private val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) - private val jsons: Seq[ToJSON[Any]] = summonToJson[mirrorOfSum.MirroredElemTypes] - private val names: Seq[String] = - constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector - .asInstanceOf[Vector[String]] - private val jsonsByNames: Map[String, ToJSON[Any]] = names.zip(jsons).toMap - - override def write(value: A): JValue = { - // we never get a trait here, only classes, it's safe to assume Product - val originalTypeName = value.asInstanceOf[Product].productPrefix - val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) - val json = jsonsByNames(originalTypeName).write(value).asInstanceOf[JObject] - val typeDiscriminator = traitMetaData.typeDiscriminator -> JString(typeName) - JObject(typeDiscriminator :: json.obj) - } - - } - - inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): ToJSON[A] = - new ToJSON[A] { - private val caseClassMetaData: CaseClassMetaData = AnnotationReader.readCaseClassMetaData[A] - private val toJsons: Vector[ToJSON[Any]] = summonToJson[mirrorOfProduct.MirroredElemTypes] - - override def write(value: A): JValue = { - val caseClassFields = value.asInstanceOf[Product].productIterator - toJsons - .zip(caseClassFields) - .zip(caseClassMetaData.fields) - .foldLeft[JValue](JObject()) { case (jObject, ((toJson, fieldValue), field)) => - addField(jObject.asInstanceOf[JObject], field, toJson.write(fieldValue)) - } - } - } - - inline private def summonToJson[T <: Tuple]: Vector[ToJSON[Any]] = - inline erasedValue[T] match { - case _: EmptyTuple => Vector.empty - case _: (t *: ts) => - summonInline[ToJSON[t]] - .asInstanceOf[ToJSON[Any]] +: summonToJson[ts] - } - } - - private val emptyJArray = JArray(Nil) - private val emptyJObject = JObject(Nil) +object ToJSON extends ToJSONInstances with ToJSONCatsInstances with generic.DeriveToJSON { inline def apply[A](implicit instance: ToJSON[A]): ToJSON[A] = instance + inline def apply[A: JSON]: ToJSON[A] = summon[ToJSON[A]] /** construct an instance from a function */ def instance[T](toJson: T => JValue): ToJSON[T] = new ToJSON[T] { override def write(value: T): JValue = toJson(value) } - - given optionWriter[A](using c: ToJSON[A]): ToJSON[Option[A]] = - new ToJSON[Option[A]] { - def write(opt: Option[A]): JValue = opt match { - case Some(a) => c.write(a) - case None => JNothing - } - } - - implicit def listWriter[A](implicit w: ToJSON[A]): ToJSON[List[A]] = - new ToJSON[List[A]] { - def write(l: List[A]): JValue = - if (l.isEmpty) emptyJArray - else JArray(l.map(w.write)) - } - - implicit def nonEmptyListWriter[A](implicit w: ToJSON[A]): ToJSON[NonEmptyList[A]] = - new ToJSON[NonEmptyList[A]] { - def write(l: NonEmptyList[A]): JValue = JArray(l.toList.map(w.write)) - } - - implicit def seqWriter[A](implicit w: ToJSON[A]): ToJSON[Seq[A]] = - new ToJSON[Seq[A]] { - def write(s: Seq[A]): JValue = - if (s.isEmpty) emptyJArray - else JArray(s.iterator.map(w.write).toList) - } - - implicit def setWriter[A](implicit w: ToJSON[A]): ToJSON[Set[A]] = - new ToJSON[Set[A]] { - def write(s: Set[A]): JValue = - if (s.isEmpty) emptyJArray - else JArray(s.iterator.map(w.write).toList) - } - - implicit def vectorWriter[A](implicit w: ToJSON[A]): ToJSON[Vector[A]] = - new ToJSON[Vector[A]] { - def write(v: Vector[A]): JValue = - if (v.isEmpty) emptyJArray - else JArray(v.iterator.map(w.write).toList) - } - - implicit val intWriter: ToJSON[Int] = new ToJSON[Int] { - def write(i: Int): JValue = JLong(i) - } - - implicit val stringWriter: ToJSON[String] = new ToJSON[String] { - def write(s: String): JValue = JString(s) - } - - implicit val bigIntWriter: ToJSON[BigInt] = new ToJSON[BigInt] { - def write(i: BigInt): JValue = JInt(i) - } - - implicit val shortWriter: ToJSON[Short] = new ToJSON[Short] { - def write(s: Short): JValue = JLong(s) - } - - implicit val longWriter: ToJSON[Long] = new ToJSON[Long] { - def write(l: Long): JValue = JLong(l) - } - - implicit val floatWriter: ToJSON[Float] = new ToJSON[Float] { - def write(f: Float): JValue = JDouble(f) - } - - implicit val doubleWriter: ToJSON[Double] = new ToJSON[Double] { - def write(d: Double): JValue = JDouble(d) - } - - implicit val booleanWriter: ToJSON[Boolean] = new ToJSON[Boolean] { - def write(b: Boolean): JValue = if (b) JBool.True else JBool.False - } - - implicit def mapWriter[A: ToJSON]: ToJSON[Map[String, A]] = new ToJSON[Map[String, A]] { - def write(m: Map[String, A]) = - if (m.isEmpty) emptyJObject - else - JObject(m.iterator.map { case (k, v) => - JField(k, toJValue(v)) - }.toList) - } - - implicit val moneyWriter: ToJSON[Money] = new ToJSON[Money] { - import Money.* - - def write(m: Money): JValue = JObject( - JField(BaseMoney.TypeField, toJValue(m.`type`)) :: - JField(CurrencyCodeField, toJValue(m.currency)) :: - JField(CentAmountField, toJValue(m.centAmount)) :: - JField(FractionDigitsField, toJValue(m.currency.getDefaultFractionDigits)) :: - Nil - ) - } - - implicit val highPrecisionMoneyWriter: ToJSON[HighPrecisionMoney] = - new ToJSON[HighPrecisionMoney] { - import HighPrecisionMoney.* - def write(m: HighPrecisionMoney): JValue = JObject( - JField(BaseMoney.TypeField, toJValue(m.`type`)) :: - JField(CurrencyCodeField, toJValue(m.currency)) :: - JField(CentAmountField, toJValue(m.centAmount)) :: - JField(PreciseAmountField, toJValue(m.preciseAmount)) :: - JField(FractionDigitsField, toJValue(m.fractionDigits)) :: - Nil - ) - } - - implicit val baseMoneyWriter: ToJSON[BaseMoney] = new ToJSON[BaseMoney] { - def write(m: BaseMoney): JValue = m match { - case m: Money => moneyWriter.write(m) - case m: HighPrecisionMoney => highPrecisionMoneyWriter.write(m) - } - } - - implicit val currencyWriter: ToJSON[Currency] = new ToJSON[Currency] { - def write(c: Currency): JValue = toJValue(c.getCurrencyCode) - } - - implicit val jValueWriter: ToJSON[JValue] = new ToJSON[JValue] { - def write(jval: JValue): JValue = jval - } - - implicit val jObjectWriter: ToJSON[JObject] = new ToJSON[JObject] { - def write(jObj: JObject): JValue = jObj - } - - implicit val unitWriter: ToJSON[Unit] = new ToJSON[Unit] { - def write(u: Unit): JValue = JNothing - } - - // Joda time - implicit val dateTimeWriter: ToJSON[DateTime] = new ToJSON[DateTime] { - def write(dt: DateTime): JValue = JString( - ISODateTimeFormat.dateTime.print(dt.withZone(DateTimeZone.UTC))) - } - - implicit val timeWriter: ToJSON[LocalTime] = new ToJSON[LocalTime] { - def write(lt: LocalTime): JValue = JString(ISODateTimeFormat.time.print(lt)) - } - - implicit val dateWriter: ToJSON[LocalDate] = new ToJSON[LocalDate] { - def write(ld: LocalDate): JValue = JString(ISODateTimeFormat.date.print(ld)) - } - - implicit val yearMonthWriter: ToJSON[YearMonth] = new ToJSON[YearMonth] { - def write(ym: YearMonth): JValue = JString(ISODateTimeFormat.yearMonth().print(ym)) - } - - // java.time - - // always format the milliseconds - private val javaInstantFormatter = new time.format.DateTimeFormatterBuilder() - .appendInstant(3) - .toFormatter() - implicit val javaInstantWriter: ToJSON[time.Instant] = new ToJSON[time.Instant] { - def write(value: time.Instant): JValue = JString( - javaInstantFormatter.format(time.OffsetDateTime.ofInstant(value, time.ZoneOffset.UTC))) - } - - // always format the milliseconds - private val javaLocalTimeFormatter = new time.format.DateTimeFormatterBuilder() - .appendPattern("HH:mm:ss.SSS") - .toFormatter() - implicit val javaTimeWriter: ToJSON[time.LocalTime] = new ToJSON[time.LocalTime] { - def write(value: time.LocalTime): JValue = JString(javaLocalTimeFormatter.format(value)) - } - - implicit val javaDateWriter: ToJSON[time.LocalDate] = new ToJSON[time.LocalDate] { - def write(value: time.LocalDate): JValue = JString( - time.format.DateTimeFormatter.ISO_LOCAL_DATE.format(value)) - } - - implicit val javaYearMonth: ToJSON[time.YearMonth] = new ToJSON[time.YearMonth] { - def write(value: time.YearMonth): JValue = JString(JavaYearMonthFormatter.format(value)) - } - - implicit val uuidWriter: ToJSON[UUID] = new ToJSON[UUID] { - def write(uuid: UUID): JValue = JString(uuid.toString) - } - - implicit val localeWriter: ToJSON[Locale] = new ToJSON[Locale] { - def write(locale: Locale): JValue = JString(locale.toLanguageTag) - } - - implicit def eitherWriter[A: ToJSON, B: ToJSON]: ToJSON[Either[A, B]] = new ToJSON[Either[A, B]] { - def write(e: Either[A, B]): JValue = e match { - case Left(l) => toJValue(l) - case Right(r) => toJValue(r) - } - } } diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala similarity index 98% rename from json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveJSON.scala rename to json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala index f4c8f39a..f3710a7b 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveJSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala @@ -13,10 +13,10 @@ import scala.deriving.Mirror import io.sphere.json.FromJSON import io.sphere.json.* -trait DeriveJSON { - inline given derived[A](using Mirror.Of[A]): FromJSON[A] = Derived.derived[A] +trait DeriveFromJSON { + inline given derived[A](using Mirror.Of[A]): FromJSON[A] = Derivation.derived[A] - object Derived { + protected object Derivation { import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala new file mode 100644 index 00000000..d9a9b099 --- /dev/null +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala @@ -0,0 +1,80 @@ +package io.sphere.json.generic + +import io.sphere.json.ToJSON +import org.json4s.JsonAST.* + +import scala.deriving.Mirror + +trait DeriveToJSON { + + inline given derived[A](using Mirror.Of[A]): ToJSON[A] = Derivation.derived[A] + + private def addField(jObject: JObject, field: Field, jValue: JValue): JValue = + jValue match { + case o: JObject => + if (field.embedded) JObject(jObject.obj ++ o.obj) + else JObject(jObject.obj :+ (field.fieldName -> o)) + case other => JObject(jObject.obj :+ (field.fieldName -> other)) + } + + protected object Derivation { + + import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} + + inline def derived[A](using m: Mirror.Of[A]): ToJSON[A] = + inline m match { + case s: Mirror.SumOf[A] => deriveTrait(s) + case p: Mirror.ProductOf[A] => deriveCaseClass(p) + } + + inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): ToJSON[A] = + new ToJSON[A] { + private val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] + private val typeHintMap: Map[String, String] = traitMetaData.subtypes.collect { + case (name, classMeta) if classMeta.typeHint.isDefined => + name -> classMeta.typeHint.get + } + private val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) + private val jsons: Seq[ToJSON[Any]] = summonToJson[mirrorOfSum.MirroredElemTypes] + private val names: Seq[String] = + constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector + .asInstanceOf[Vector[String]] + private val jsonsByNames: Map[String, ToJSON[Any]] = names.zip(jsons).toMap + + override def write(value: A): JValue = { + // we never get a trait here, only classes, it's safe to assume Product + val originalTypeName = value.asInstanceOf[Product].productPrefix + val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) + val json = jsonsByNames(originalTypeName).write(value).asInstanceOf[JObject] + val typeDiscriminator = traitMetaData.typeDiscriminator -> JString(typeName) + JObject(typeDiscriminator :: json.obj) + } + + } + + inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): ToJSON[A] = + new ToJSON[A] { + private val caseClassMetaData: CaseClassMetaData = AnnotationReader.readCaseClassMetaData[A] + private val toJsons: Vector[ToJSON[Any]] = summonToJson[mirrorOfProduct.MirroredElemTypes] + + override def write(value: A): JValue = { + val caseClassFields = value.asInstanceOf[Product].productIterator + toJsons + .zip(caseClassFields) + .zip(caseClassMetaData.fields) + .foldLeft[JValue](JObject()) { case (jObject, ((toJson, fieldValue), field)) => + addField(jObject.asInstanceOf[JObject], field, toJson.write(fieldValue)) + } + } + } + + inline private def summonToJson[T <: Tuple]: Vector[ToJSON[Any]] = + inline erasedValue[T] match { + case _: EmptyTuple => Vector.empty + case _: (t *: ts) => + summonInline[ToJSON[t]] + .asInstanceOf[ToJSON[Any]] +: summonToJson[ts] + } + } + +} diff --git a/json/json-core/src/main/scala/io/sphere/json/ToJSONInstances.scala b/json/json-core/src/main/scala/io/sphere/json/ToJSONInstances.scala new file mode 100644 index 00000000..5d389e80 --- /dev/null +++ b/json/json-core/src/main/scala/io/sphere/json/ToJSONInstances.scala @@ -0,0 +1,214 @@ +package io.sphere.json + +import cats.data.NonEmptyList +import java.util.{Currency, Locale, UUID} +import java.time + +import io.sphere.util.{BaseMoney, HighPrecisionMoney, Money} +import org.json4s.JsonAST._ +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import org.joda.time.LocalTime +import org.joda.time.LocalDate +import org.joda.time.YearMonth +import org.joda.time.format.ISODateTimeFormat + +trait ToJSONInstances { + + private val emptyJArray = JArray(Nil) + private val emptyJObject = JObject(Nil) + + implicit def optionWriter[@specialized A](implicit c: ToJSON[A]): ToJSON[Option[A]] = + new ToJSON[Option[A]] { + def write(opt: Option[A]): JValue = opt match { + case Some(a) => c.write(a) + case None => JNothing + } + } + + implicit def listWriter[@specialized A](implicit w: ToJSON[A]): ToJSON[List[A]] = + new ToJSON[List[A]] { + def write(l: List[A]): JValue = + if (l.isEmpty) emptyJArray + else JArray(l.map(w.write)) + } + + implicit def nonEmptyListWriter[A](implicit w: ToJSON[A]): ToJSON[NonEmptyList[A]] = + new ToJSON[NonEmptyList[A]] { + def write(l: NonEmptyList[A]): JValue = JArray(l.toList.map(w.write)) + } + + implicit def seqWriter[@specialized A](implicit w: ToJSON[A]): ToJSON[Seq[A]] = + new ToJSON[Seq[A]] { + def write(s: Seq[A]): JValue = + if (s.isEmpty) emptyJArray + else JArray(s.iterator.map(w.write).toList) + } + + implicit def setWriter[@specialized A](implicit w: ToJSON[A]): ToJSON[Set[A]] = + new ToJSON[Set[A]] { + def write(s: Set[A]): JValue = + if (s.isEmpty) emptyJArray + else JArray(s.iterator.map(w.write).toList) + } + + implicit def vectorWriter[@specialized A](implicit w: ToJSON[A]): ToJSON[Vector[A]] = + new ToJSON[Vector[A]] { + def write(v: Vector[A]): JValue = + if (v.isEmpty) emptyJArray + else JArray(v.iterator.map(w.write).toList) + } + + implicit val intWriter: ToJSON[Int] = new ToJSON[Int] { + def write(i: Int): JValue = JLong(i) + } + + implicit val stringWriter: ToJSON[String] = new ToJSON[String] { + def write(s: String): JValue = JString(s) + } + + implicit val bigIntWriter: ToJSON[BigInt] = new ToJSON[BigInt] { + def write(i: BigInt): JValue = JInt(i) + } + + implicit val shortWriter: ToJSON[Short] = new ToJSON[Short] { + def write(s: Short): JValue = JLong(s) + } + + implicit val longWriter: ToJSON[Long] = new ToJSON[Long] { + def write(l: Long): JValue = JLong(l) + } + + implicit val floatWriter: ToJSON[Float] = new ToJSON[Float] { + def write(f: Float): JValue = JDouble(f) + } + + implicit val doubleWriter: ToJSON[Double] = new ToJSON[Double] { + def write(d: Double): JValue = JDouble(d) + } + + implicit val booleanWriter: ToJSON[Boolean] = new ToJSON[Boolean] { + def write(b: Boolean): JValue = if (b) JBool.True else JBool.False + } + + implicit def mapWriter[A: ToJSON]: ToJSON[Map[String, A]] = new ToJSON[Map[String, A]] { + def write(m: Map[String, A]) = + if (m.isEmpty) emptyJObject + else + JObject(m.iterator.map { case (k, v) => + JField(k, toJValue(v)) + }.toList) + } + + implicit val moneyWriter: ToJSON[Money] = new ToJSON[Money] { + + import Money._ + + def write(m: Money): JValue = JObject( + JField(BaseMoney.TypeField, toJValue(m.`type`)) :: + JField(CurrencyCodeField, toJValue(m.currency)) :: + JField(CentAmountField, toJValue(m.centAmount)) :: + JField(FractionDigitsField, toJValue(m.currency.getDefaultFractionDigits)) :: + Nil + ) + } + + implicit val highPrecisionMoneyWriter: ToJSON[HighPrecisionMoney] = + new ToJSON[HighPrecisionMoney] { + + import HighPrecisionMoney._ + + def write(m: HighPrecisionMoney): JValue = JObject( + JField(BaseMoney.TypeField, toJValue(m.`type`)) :: + JField(CurrencyCodeField, toJValue(m.currency)) :: + JField(CentAmountField, toJValue(m.centAmount)) :: + JField(PreciseAmountField, toJValue(m.preciseAmount)) :: + JField(FractionDigitsField, toJValue(m.fractionDigits)) :: + Nil + ) + } + + implicit val baseMoneyWriter: ToJSON[BaseMoney] = new ToJSON[BaseMoney] { + def write(m: BaseMoney): JValue = m match { + case m: Money => moneyWriter.write(m) + case m: HighPrecisionMoney => highPrecisionMoneyWriter.write(m) + } + } + + implicit val currencyWriter: ToJSON[Currency] = new ToJSON[Currency] { + def write(c: Currency): JValue = toJValue(c.getCurrencyCode) + } + + implicit val jValueWriter: ToJSON[JValue] = new ToJSON[JValue] { + def write(jval: JValue): JValue = jval + } + + implicit val jObjectWriter: ToJSON[JObject] = new ToJSON[JObject] { + def write(jObj: JObject): JValue = jObj + } + + implicit val unitWriter: ToJSON[Unit] = new ToJSON[Unit] { + def write(u: Unit): JValue = JNothing + } + + // Joda time + implicit val dateTimeWriter: ToJSON[DateTime] = new ToJSON[DateTime] { + def write(dt: DateTime): JValue = JString( + ISODateTimeFormat.dateTime.print(dt.withZone(DateTimeZone.UTC))) + } + + implicit val timeWriter: ToJSON[LocalTime] = new ToJSON[LocalTime] { + def write(lt: LocalTime): JValue = JString(ISODateTimeFormat.time.print(lt)) + } + + implicit val dateWriter: ToJSON[LocalDate] = new ToJSON[LocalDate] { + def write(ld: LocalDate): JValue = JString(ISODateTimeFormat.date.print(ld)) + } + + implicit val yearMonthWriter: ToJSON[YearMonth] = new ToJSON[YearMonth] { + def write(ym: YearMonth): JValue = JString(ISODateTimeFormat.yearMonth().print(ym)) + } + + // java.time + + // always format the milliseconds + private val javaInstantFormatter = new time.format.DateTimeFormatterBuilder() + .appendInstant(3) + .toFormatter() + implicit val javaInstantWriter: ToJSON[time.Instant] = new ToJSON[time.Instant] { + def write(value: time.Instant): JValue = JString( + javaInstantFormatter.format(time.OffsetDateTime.ofInstant(value, time.ZoneOffset.UTC))) + } + + // always format the milliseconds + private val javaLocalTimeFormatter = new time.format.DateTimeFormatterBuilder() + .appendPattern("HH:mm:ss.SSS") + .toFormatter() + implicit val javaTimeWriter: ToJSON[time.LocalTime] = new ToJSON[time.LocalTime] { + def write(value: time.LocalTime): JValue = JString(javaLocalTimeFormatter.format(value)) + } + + implicit val javaDateWriter: ToJSON[time.LocalDate] = new ToJSON[time.LocalDate] { + def write(value: time.LocalDate): JValue = JString( + time.format.DateTimeFormatter.ISO_LOCAL_DATE.format(value)) + } + + implicit val javaYearMonth: ToJSON[time.YearMonth] = new ToJSON[time.YearMonth] { + def write(value: time.YearMonth): JValue = JString(JavaYearMonthFormatter.format(value)) + } + + implicit val uuidWriter: ToJSON[UUID] = new ToJSON[UUID] { + def write(uuid: UUID): JValue = JString(uuid.toString) + } + + implicit val localeWriter: ToJSON[Locale] = new ToJSON[Locale] { + def write(locale: Locale): JValue = JString(locale.toLanguageTag) + } + + implicit def eitherWriter[A: ToJSON, B: ToJSON]: ToJSON[Either[A, B]] = new ToJSON[Either[A, B]] { + def write(e: Either[A, B]): JValue = e match { + case Left(l) => toJValue(l) + case Right(r) => toJValue(r) + } + } +} \ No newline at end of file From c0c45998ed977734cf773e0acfeb647447a3da5c Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 25 Apr 2025 12:54:42 +0200 Subject: [PATCH 083/142] Move MongoFormat instances to a common file (between scala 2/3) --- .../mongo/format/DefaultMongoFormats.scala | 271 ------------------ .../io/sphere/mongo/format/MongoFormat.scala | 2 - .../mongo/format/DefaultMongoFormats.scala | 0 .../format/DefaultMongoFormatsTest.scala | 2 +- 4 files changed, 1 insertion(+), 274 deletions(-) delete mode 100644 mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/DefaultMongoFormats.scala rename mongo/mongo-core/src/main/{scala-2 => scala}/io/sphere/mongo/format/DefaultMongoFormats.scala (100%) diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/DefaultMongoFormats.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/DefaultMongoFormats.scala deleted file mode 100644 index 2fb2eff7..00000000 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/DefaultMongoFormats.scala +++ /dev/null @@ -1,271 +0,0 @@ -package io.sphere.mongo.format - -import com.mongodb.BasicDBObject -import io.sphere.mongo.format -import io.sphere.mongo.format.SimpleMongoType -import io.sphere.util.{BaseMoney, HighPrecisionMoney, LangTag, Money} -import org.bson.{BSONObject, BasicBSONObject} -import org.bson.types.{BasicBSONList, ObjectId} - -import java.util.{Currency, Locale, UUID} -import java.util.regex.Pattern -import scala.collection.immutable.VectorBuilder -import scala.collection.mutable.ListBuffer - -object DefaultMongoFormats extends DefaultMongoFormats {} - -trait DefaultMongoFormats { - given uuidFormat: MongoFormat[UUID] = new NativeMongoFormat[UUID] - given objectIdFormat: MongoFormat[ObjectId] = new NativeMongoFormat[ObjectId] - given stringFormat: MongoFormat[String] = new NativeMongoFormat[String] - given shortFormat: MongoFormat[Short] = new NativeMongoFormat[Short] - given intFormat: MongoFormat[Int] = new NativeMongoFormat[Int] - given longFormat: MongoFormat[Long] = new MongoFormat[Long] { - private val native = new NativeMongoFormat[Long] - - override def toMongoValue(a: Long): Any = native.toMongoValue(a) - - override def fromMongoValue(any: Any): Long = - any match { - // a Long can read from an Int (for example, old aggregates version) - case i: Int => intFormat.fromMongoValue(i) - case _ => native.fromMongoValue(any) - } - } - given floatFormat: MongoFormat[Float] = new NativeMongoFormat[Float] - given doubleFormat: MongoFormat[Double] = new NativeMongoFormat[Double] - given booleanFormat: MongoFormat[Boolean] = new NativeMongoFormat[Boolean] - given patternFormat: MongoFormat[Pattern] = new NativeMongoFormat[Pattern] - - given optionFormat[A](using format: MongoFormat[A]): MongoFormat[Option[A]] = - new MongoFormat[Option[A]] { - override def toMongoValue(a: Option[A]): Any = - a match { - case Some(value) => format.toMongoValue(value) - case None => MongoNothing - } - - override def fromMongoValue(mongoType: Any): Option[A] = { - import scala.jdk.CollectionConverters.* - val fieldNames = format.fields - if (mongoType == null) None - else - mongoType match { - case s: SimpleMongoType => Some(format.fromMongoValue(s)) - case bson: BasicDBObject => - val bsonFieldNames = bson.keySet().asScala - if (fieldNames.nonEmpty && bsonFieldNames.intersect(fieldNames).isEmpty) None - else Some(format.fromMongoValue(bson)) - case MongoNothing => None // This can't happen, but it makes the compiler happy - } - } - - override def default: Option[Option[A]] = Some(None) - } - - given vecFormat[@specialized A](using format: MongoFormat[A]): MongoFormat[Vector[A]] = - new MongoFormat[Vector[A]] { - import scala.collection.JavaConverters._ - override def toMongoValue(a: Vector[A]) = { - val m = new BasicBSONList() - if (a.nonEmpty) - m.addAll(a.map(format.toMongoValue(_).asInstanceOf[AnyRef]).asJavaCollection) - m - } - override def fromMongoValue(any: Any): Vector[A] = - any match { - case l: BasicBSONList => - if (l.isEmpty) Vector.empty - else { - val builder = new VectorBuilder[A] - val iter = l.iterator() - while (iter.hasNext) { - val element = iter.next() - builder += format.fromMongoValue(element) - } - builder.result() - } - case _ => throw new Exception(s"cannot read value from ${any.getClass.getName}") - } - } - - given listFormat[@specialized A](using format: MongoFormat[A]): MongoFormat[List[A]] = - new MongoFormat[List[A]] { - import scala.collection.JavaConverters._ - override def toMongoValue(a: List[A]) = { - val m = new BasicBSONList() - if (a.nonEmpty) - m.addAll(a.map(format.toMongoValue(_).asInstanceOf[AnyRef]).asJavaCollection) - m - } - override def fromMongoValue(any: Any): List[A] = - any match { - case l: BasicBSONList => - if (l.isEmpty) Nil - else { - val builder = new ListBuffer[A] - val iter = l.iterator() - while (iter.hasNext) { - val element = iter.next() - builder += format.fromMongoValue(element) - } - builder.result() - } - case _ => throw new Exception(s"cannot read value from ${any.getClass.getName}") - } - } - - given setFormat[@specialized A](using f: MongoFormat[A]): MongoFormat[Set[A]] = - new MongoFormat[Set[A]] { - import scala.collection.JavaConverters._ - override def toMongoValue(a: Set[A]) = { - val m = new BasicBSONList() - if (a.nonEmpty) - m.addAll(a.map(f.toMongoValue(_).asInstanceOf[AnyRef]).asJavaCollection) - m - } - override def fromMongoValue(any: Any): Set[A] = - any match { - case l: BasicBSONList => - if (l.isEmpty) Set.empty - else l.iterator().asScala.map(f.fromMongoValue).toSet - case _ => throw new Exception(s"cannot read value from ${any.getClass.getName}") - } - } - - given mapFormat[@specialized A](using f: MongoFormat[A]): MongoFormat[Map[String, A]] = - new MongoFormat[Map[String, A]] { - override def toMongoValue(map: Map[String, A]): Any = - // Perf note: new BasicBSONObject(map.size) is much slower for some reason - map.foldLeft(new BasicBSONObject()) { case (dbo, (k, v)) => - dbo.append(k, summon[MongoFormat[A]].toMongoValue(v)) - } - - override def fromMongoValue(any: Any): Map[String, A] = { - import scala.language.existentials - - val map: java.util.Map[?, ?] = any match { - case b: BasicBSONObject => b // avoid instantiating a new map - case dbo: BSONObject => dbo.toMap - case other => throw new Exception(s"cannot read value from ${other.getClass.getName}") - } - val builder = Map.newBuilder[String, A] - val iter = map.entrySet().iterator() - while (iter.hasNext) { - val entry = iter.next() - val k = entry.getKey.asInstanceOf[String] - val v = summon[MongoFormat[A]].fromMongoValue(entry.getValue) - builder += (k -> v) - } - builder.result() - } - } - - given currencyFormat: MongoFormat[Currency] = new MongoFormat[Currency] { - val failMsg = "ISO 4217 code JSON String expected." - def failMsgFor(input: String) = s"Currency '$input' not valid as ISO 4217 code." - - override def toMongoValue(c: Currency): Any = c.getCurrencyCode - override def fromMongoValue(any: Any): Currency = any match { - case s: String => - try Currency.getInstance(s) - catch { - case _: IllegalArgumentException => throw new Exception(failMsgFor(s)) - } - case _ => throw new Exception(failMsg) - } - } - - given moneyFormat: MongoFormat[Money] = new MongoFormat[Money] { - import Money._ - - override val fields = Set(CentAmountField, CurrencyCodeField) - - override def toMongoValue(m: Money): Any = - new BasicBSONObject() - .append(BaseMoney.TypeField, m.`type`) - .append(CurrencyCodeField, currencyFormat.toMongoValue(m.currency)) - .append(CentAmountField, longFormat.toMongoValue(m.centAmount)) - .append(FractionDigitsField, m.currency.getDefaultFractionDigits) - - override def fromMongoValue(any: Any): Money = any match { - case dbo: BSONObject => - Money.fromCentAmount( - field[Long](CentAmountField, dbo), - field[Currency](CurrencyCodeField, dbo)) - case other => throw new Exception(s"db object expected but has '${other.getClass.getName}'") - } - } - - given highPrecisionMoneyFormat: MongoFormat[HighPrecisionMoney] = - new MongoFormat[HighPrecisionMoney] { - import HighPrecisionMoney._ - - override val fields = Set(PreciseAmountField, CurrencyCodeField, FractionDigitsField) - - override def toMongoValue(m: HighPrecisionMoney): Any = - new BasicBSONObject() - .append(BaseMoney.TypeField, m.`type`) - .append(CurrencyCodeField, currencyFormat.toMongoValue(m.currency)) - .append(CentAmountField, longFormat.toMongoValue(m.centAmount)) - .append(PreciseAmountField, longFormat.toMongoValue(m.preciseAmount)) - .append(FractionDigitsField, m.fractionDigits) - override def fromMongoValue(any: Any): HighPrecisionMoney = any match { - case dbo: BSONObject => - HighPrecisionMoney - .fromPreciseAmount( - field[Long](PreciseAmountField, dbo), - field[Int](FractionDigitsField, dbo), - field[Currency](CurrencyCodeField, dbo), - field[Option[Long]](CentAmountField, dbo) - ) - .fold(nel => throw new Exception(nel.toList.mkString(", ")), identity) - - case other => throw new Exception(s"db object expected but has '${other.getClass.getName}'") - } - } - - given baseMoneyFormat: MongoFormat[BaseMoney] = new MongoFormat[BaseMoney] { - override def toMongoValue(a: BaseMoney): Any = a match { - case m: Money => moneyFormat.toMongoValue(m) - case m: HighPrecisionMoney => highPrecisionMoneyFormat.toMongoValue(m) - } - override def fromMongoValue(any: Any): BaseMoney = any match { - case dbo: BSONObject => - val typeField = dbo.get(BaseMoney.TypeField) - if (typeField == null) - moneyFormat.fromMongoValue(any) - else - stringFormat.fromMongoValue(typeField) match { - case Money.TypeName => moneyFormat.fromMongoValue(any) - case HighPrecisionMoney.TypeName => - highPrecisionMoneyFormat.fromMongoValue(any) - case tpe => - throw new Exception( - s"Unknown money type '$tpe'. Available types are: '${Money.TypeName}', '${HighPrecisionMoney.TypeName}'.") - } - case other => throw new Exception(s"db object expected but has '${other.getClass.getName}'") - } - } - - given localeFormat: MongoFormat[Locale] = new MongoFormat[Locale] { - override def toMongoValue(a: Locale): Any = a.toLanguageTag - override def fromMongoValue(any: Any): Locale = any match { - case s: String => - s match { - case LangTag(langTag) => langTag - case _ => - if (LangTag.unapply(s).isEmpty) - throw new Exception("Undefined locale is not allowed") - else - throw new Exception(LangTag.invalidLangTagMessage(s)) - } - case _ => - throw new Exception( - s"Locale is expected to be of type String but has '${any.getClass.getName}'") - } - } - - private def field[A](name: String, dbo: BSONObject)(implicit format: MongoFormat[A]): A = - format.fromMongoValue(dbo.get(name)) -} diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala index 49001712..462afe9d 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala @@ -8,8 +8,6 @@ import java.util.UUID import java.util.regex.Pattern import scala.deriving.Mirror -object MongoNothing - type SimpleMongoType = UUID | String | ObjectId | Short | Int | Long | Float | Double | Boolean | Pattern diff --git a/mongo/mongo-core/src/main/scala-2/io/sphere/mongo/format/DefaultMongoFormats.scala b/mongo/mongo-core/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala similarity index 100% rename from mongo/mongo-core/src/main/scala-2/io/sphere/mongo/format/DefaultMongoFormats.scala rename to mongo/mongo-core/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/DefaultMongoFormatsTest.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/DefaultMongoFormatsTest.scala index 5d483ee8..49d17a04 100644 --- a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/DefaultMongoFormatsTest.scala +++ b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/DefaultMongoFormatsTest.scala @@ -3,7 +3,7 @@ package io.sphere.mongo.format import java.util.Locale import com.mongodb.DBObject import io.sphere.mongo.MongoUtils -import io.sphere.mongo.format.DefaultMongoFormats.given +import io.sphere.mongo.format.DefaultMongoFormats.* import io.sphere.util.LangTag import org.bson.BasicBSONObject import org.bson.types.BasicBSONList From 2172bf6a1a3e39ef3758b8f8e2395ca3accb937d Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 25 Apr 2025 12:59:21 +0200 Subject: [PATCH 084/142] formatting --- .../src/main/scala/io/sphere/json/ToJSONInstances.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/json/json-core/src/main/scala/io/sphere/json/ToJSONInstances.scala b/json/json-core/src/main/scala/io/sphere/json/ToJSONInstances.scala index 5d389e80..71e32db7 100644 --- a/json/json-core/src/main/scala/io/sphere/json/ToJSONInstances.scala +++ b/json/json-core/src/main/scala/io/sphere/json/ToJSONInstances.scala @@ -211,4 +211,4 @@ trait ToJSONInstances { case Right(r) => toJValue(r) } } -} \ No newline at end of file +} From 1c81994042aea83622b8838be2e48c8f560ae96a Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 25 Apr 2025 13:06:27 +0200 Subject: [PATCH 085/142] Move common values to objects --- .../src/main/scala/io/sphere/json/FromJSONInstances.scala | 7 +++++-- .../src/main/scala/io/sphere/json/ToJSONInstances.scala | 7 +++++-- .../main/scala-3/io/sphere/mongo/format/MongoFormat.scala | 4 ---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/json/json-core/src/main/scala/io/sphere/json/FromJSONInstances.scala b/json/json-core/src/main/scala/io/sphere/json/FromJSONInstances.scala index 5537104d..0b555e13 100644 --- a/json/json-core/src/main/scala/io/sphere/json/FromJSONInstances.scala +++ b/json/json-core/src/main/scala/io/sphere/json/FromJSONInstances.scala @@ -18,14 +18,17 @@ import org.joda.time.LocalTime import org.joda.time.LocalDate import org.joda.time.format.ISODateTimeFormat -trait FromJSONInstances { - +object FromJSONInstances { private val validNone = Valid(None) private val validNil = Valid(Nil) private val validEmptyAnyVector: Valid[Vector[Any]] = Valid(Vector.empty) private def validList[A]: Valid[List[A]] = validNil private def validEmptyVector[A]: Valid[Vector[A]] = validEmptyAnyVector.asInstanceOf[Valid[Vector[A]]] +} + +trait FromJSONInstances { + import FromJSONInstances._ implicit def optionMapReader[@specialized A](implicit c: FromJSON[A]): FromJSON[Option[Map[String, A]]] = diff --git a/json/json-core/src/main/scala/io/sphere/json/ToJSONInstances.scala b/json/json-core/src/main/scala/io/sphere/json/ToJSONInstances.scala index 71e32db7..7e6df798 100644 --- a/json/json-core/src/main/scala/io/sphere/json/ToJSONInstances.scala +++ b/json/json-core/src/main/scala/io/sphere/json/ToJSONInstances.scala @@ -13,10 +13,13 @@ import org.joda.time.LocalDate import org.joda.time.YearMonth import org.joda.time.format.ISODateTimeFormat -trait ToJSONInstances { - +object ToJSONInstances { private val emptyJArray = JArray(Nil) private val emptyJObject = JObject(Nil) +} + +trait ToJSONInstances { + import ToJSONInstances._ implicit def optionWriter[@specialized A](implicit c: ToJSON[A]): ToJSON[Option[A]] = new ToJSON[Option[A]] { diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala index 462afe9d..dd8091ed 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala @@ -20,10 +20,6 @@ trait MongoFormat[A] extends Serializable { def default: Option[A] = None } -final class NativeMongoFormat[A] extends MongoFormat[A] { - def toMongoValue(a: A): Any = a - def fromMongoValue(any: Any): A = any.asInstanceOf[A] -} inline def deriveMongoFormat[A](using Mirror.Of[A]): MongoFormat[A] = MongoFormat.derived From 436f004509ce3d64ea236cec227f290d9a18a310 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 25 Apr 2025 13:53:27 +0200 Subject: [PATCH 086/142] Remove duplicated test files --- .../format/BaseMoneyMongoFormatTest.scala | 98 ------------ .../format/DefaultMongoFormatsTest.scala | 150 ------------------ .../format/BaseMoneyMongoFormatTest.scala | 0 .../format/DefaultMongoFormatsTest.scala | 0 4 files changed, 248 deletions(-) delete mode 100644 mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala delete mode 100644 mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/DefaultMongoFormatsTest.scala rename mongo/mongo-core/src/test/{scala-2 => scala}/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala (100%) rename mongo/mongo-core/src/test/{scala-2 => scala}/io/sphere/mongo/format/DefaultMongoFormatsTest.scala (100%) diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala deleted file mode 100644 index 9b66bdc7..00000000 --- a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala +++ /dev/null @@ -1,98 +0,0 @@ -package io.sphere.mongo.format - -import java.util.Currency - -import io.sphere.util.{BaseMoney, HighPrecisionMoney, Money} -import DefaultMongoFormats.given -import io.sphere.mongo.MongoUtils._ -import org.bson.BSONObject -import org.scalatest.wordspec.AnyWordSpec -import org.scalatest.matchers.should.Matchers - -import scala.collection.JavaConverters._ - -class BaseMoneyMongoFormatTest extends AnyWordSpec with Matchers { - - "MongoFormat[BaseMoney]" should { - "be symmetric" in { - val money = Money.EUR(34.56) - val f = MongoFormat[Money] - val dbo = f.toMongoValue(money) - val readMoney = f.fromMongoValue(dbo) - - money should be(readMoney) - } - - "decode with type info" in { - val dbo = dbObj( - "type" -> "centPrecision", - "currencyCode" -> "USD", - "centAmount" -> 3298 - ) - - MongoFormat[BaseMoney].fromMongoValue(dbo) should be(Money.USD(BigDecimal("32.98"))) - } - - "decode without type info" in { - val dbo = dbObj( - "currencyCode" -> "USD", - "centAmount" -> 3298 - ) - - MongoFormat[BaseMoney].fromMongoValue(dbo) should be(Money.USD(BigDecimal("32.98"))) - } - } - - "MongoFormat[HighPrecisionMoney]" should { - "be symmetric" in { - implicit val mode = BigDecimal.RoundingMode.HALF_EVEN - - val money = HighPrecisionMoney.fromDecimalAmount(34.123456, 6, Currency.getInstance("EUR")) - val dbo = MongoFormat[HighPrecisionMoney].toMongoValue(money) - - val decodedMoney = MongoFormat[HighPrecisionMoney].fromMongoValue(dbo) - val decodedBaseMoney = MongoFormat[BaseMoney].fromMongoValue(dbo) - - decodedMoney should equal(money) - decodedBaseMoney should equal(money) - } - - "decode with type info" in { - val dbo = dbObj( - "type" -> "highPrecision", - "currencyCode" -> "USD", - "preciseAmount" -> 42, - "fractionDigits" -> 4 - ) - - MongoFormat[BaseMoney].fromMongoValue(dbo) should be( - HighPrecisionMoney.USD(BigDecimal("0.0042"), Some(4))) - } - - "decode with centAmount" in { - val dbo = dbObj( - "type" -> "highPrecision", - "currencyCode" -> "USD", - "preciseAmount" -> 42, - "centAmount" -> 1, - "fractionDigits" -> 4 - ) - - val parsed = MongoFormat[BaseMoney].fromMongoValue(dbo) - MongoFormat[BaseMoney].toMongoValue(parsed).asInstanceOf[BSONObject].toMap.asScala should be( - dbo.toMap.asScala) - } - - "validate data when decoded from JSON" in { - val dbo = dbObj( - "type" -> "highPrecision", - "currencyCode" -> "USD", - "preciseAmount" -> 42, - "fractionDigits" -> 1 - ) - - an[Exception] shouldBe thrownBy(MongoFormat[BaseMoney].fromMongoValue(dbo)) - } - } - -} diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/DefaultMongoFormatsTest.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/DefaultMongoFormatsTest.scala deleted file mode 100644 index 49d17a04..00000000 --- a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/DefaultMongoFormatsTest.scala +++ /dev/null @@ -1,150 +0,0 @@ -package io.sphere.mongo.format - -import java.util.Locale -import com.mongodb.DBObject -import io.sphere.mongo.MongoUtils -import io.sphere.mongo.format.DefaultMongoFormats.* -import io.sphere.util.LangTag -import org.bson.BasicBSONObject -import org.bson.types.BasicBSONList -import org.scalacheck.Gen -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec -import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks - -import scala.collection.JavaConverters._ - -object DefaultMongoFormatsTest { - case class User(name: String) - object User { - implicit val mongo: MongoFormat[User] = new MongoFormat[User] { - override def toMongoValue(a: User): Any = MongoUtils.dbObj("name" -> a.name) - override def fromMongoValue(any: Any): User = any match { - case dbo: DBObject => - User(dbo.get("name").asInstanceOf[String]) - case _ => throw new Exception("expected DBObject") - } - } - } -} - -class DefaultMongoFormatsTest - extends AnyWordSpec - with Matchers - with ScalaCheckDrivenPropertyChecks { - import DefaultMongoFormatsTest._ - - "DefaultMongoFormats" must { - "support List[String]" in { - val format = listFormat[String] - val list = Gen.listOf(Gen.alphaNumStr) - - forAll(list) { l => - val dbo = format.toMongoValue(l) - dbo.asInstanceOf[BasicBSONList].asScala.toList must be(l) - val resultList = format.fromMongoValue(dbo) - resultList must be(l) - } - } - - "support List[A: MongoFormat]" in { - val format = listFormat[User] - val list = Gen.listOf(Gen.alphaNumStr.map(User.apply)) - - check(list, format) - } - - "support Vector[String]" in { - val format = vecFormat[String] - val vector = Gen.listOf(Gen.alphaNumStr).map(_.toVector) - - forAll(vector) { v => - val dbo = format.toMongoValue(v) - dbo.asInstanceOf[BasicBSONList].asScala.toVector must be(v) - val resultVector = format.fromMongoValue(dbo) - resultVector must be(v) - } - } - - "support Vector[A: MongoFormat]" in { - val format = vecFormat[User] - val vector = Gen.listOf(Gen.alphaNumStr.map(User.apply)).map(_.toVector) - - check(vector, format) - } - - "support Set[String]" in { - val format = setFormat[String] - val set = Gen.listOf(Gen.alphaNumStr).map(_.toSet) - - forAll(set) { s => - val dbo = format.toMongoValue(s) - dbo.asInstanceOf[BasicBSONList].asScala.toSet must be(s) - val resultSet = format.fromMongoValue(dbo) - resultSet must be(s) - } - } - - "support Set[A: MongoFormat]" in { - val format = setFormat[User] - val set = Gen.listOf(Gen.alphaNumStr.map(User.apply)).map(_.toSet) - - check(set, format) - } - - "support Map[String, String]" in { - val format = mapFormat[String] - val map = Gen - .listOf { - for { - key <- Gen.alphaNumStr - value <- Gen.alphaNumStr - } yield (key, value) - } - .map(_.toMap) - - forAll(map) { m => - val dbo = format.toMongoValue(m) - dbo.asInstanceOf[BasicBSONObject].asScala must be(m) - val resultMap = format.fromMongoValue(dbo) - resultMap must be(m) - } - } - - "support Map[String, A: MongoFormat]" in { - val format = mapFormat[User] - val map = Gen - .listOf { - for { - key <- Gen.alphaNumStr - value <- Gen.alphaNumStr.map(User.apply) - } yield (key, value) - } - .map(_.toMap) - - check(map, format) - } - - "support Java Locale" in { - Locale.getAvailableLocales.filter(_.toLanguageTag != LangTag.UNDEFINED).foreach { - (l: Locale) => - localeFormat.fromMongoValue(localeFormat.toMongoValue(l)).toLanguageTag must be( - l.toLanguageTag) - } - } - - "support UUID" in { - val format = uuidFormat - val uuids = Gen.uuid - - check(uuids, format) - } - } - - private def check[A](gen: Gen[A], format: MongoFormat[A]) = - forAll(gen) { value => - val dbo = format.toMongoValue(value) - val result = format.fromMongoValue(dbo) - result must be(value) - } -} diff --git a/mongo/mongo-core/src/test/scala-2/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala b/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala similarity index 100% rename from mongo/mongo-core/src/test/scala-2/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala rename to mongo/mongo-core/src/test/scala/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala diff --git a/mongo/mongo-core/src/test/scala-2/io/sphere/mongo/format/DefaultMongoFormatsTest.scala b/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/DefaultMongoFormatsTest.scala similarity index 100% rename from mongo/mongo-core/src/test/scala-2/io/sphere/mongo/format/DefaultMongoFormatsTest.scala rename to mongo/mongo-core/src/test/scala/io/sphere/mongo/format/DefaultMongoFormatsTest.scala From 6beea229afd466290054cdc9aa217a8c1a2ac6c9 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 25 Apr 2025 14:10:08 +0200 Subject: [PATCH 087/142] Enabling some test for scala 3 --- .../io/sphere/mongo/SerializationTest.scala | 4 ++++ .../mongo/format/OptionMongoFormatSpec.scala | 24 +++++++++---------- .../mongo/generic/SumTypesDerivingSpec.scala | 1 + 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/SerializationTest.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/SerializationTest.scala index 3af167c5..f5ac3e58 100644 --- a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/SerializationTest.scala +++ b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/SerializationTest.scala @@ -177,6 +177,10 @@ class SerializationTest extends AnyWordSpec with Matchers { "serialize and deserialize enumerations" in { val mongo: MongoFormat[Color.Value] = AnnotationReader.mongoEnum(Color) + val colors = List(Color.Red, Color.Yellow, Color.Blue) + val roundTripColors = colors.map(mongo.toMongoValue).map(mongo.fromMongoValue) + colors must be (roundTripColors) + // mongo java driver knows how to encode/decode Strings val serializedObject = mongo.toMongoValue(Color.Red).asInstanceOf[String] serializedObject must be("Red") diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/OptionMongoFormatSpec.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/OptionMongoFormatSpec.scala index cf56d827..5f3055af 100644 --- a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/OptionMongoFormatSpec.scala +++ b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/OptionMongoFormatSpec.scala @@ -65,18 +65,18 @@ class OptionMongoFormatSpec extends AnyWordSpec with Matchers with OptionValues result mustEqual None } -// "consider all fields if the data type does not impose any restriction" in { -// val dbo = dbObj( -// "key1" -> "value1", -// "key2" -> "value2" -// ) -// val expected = Map("key1" -> "value1", "key2" -> "value2") -// val result = deriveMongoFormat[Map[String, String]].fromMongoValue(dbo) -// result mustEqual expected -// -// val maybeResult = deriveMongoFormat[Option[Map[String, String]]].fromMongoValue(dbo) -// maybeResult.value mustEqual expected -// } + "consider all fields if the data type does not impose any restriction" in { + val dbo = dbObj( + "key1" -> "value1", + "key2" -> "value2" + ) + val expected = Map("key1" -> "value1", "key2" -> "value2") + val result = MongoFormat[Map[String, String]].fromMongoValue(dbo) + result mustEqual expected + + val maybeResult = MongoFormat[Option[Map[String, String]]].fromMongoValue(dbo) + maybeResult.value mustEqual expected + } "parse optional element" in { val dbo = dbObj( diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala index 3dd8944e..dfe93126 100644 --- a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala +++ b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala @@ -39,6 +39,7 @@ class SumTypesDerivingSpec extends AnyWordSpec with Matchers { "not allow specifying different custom field" in pendingUntilFixed { // to serialize Custom, should we use type "color" or "color-custom"? + // The current implementation just takes the type hint of the trait and doesn't look at the subtypes anyway "deriveMongoFormat[Color5]" mustNot compile } From 42ed16c5eba517c42f6b79145b1bc87c511aeafa Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 25 Apr 2025 14:30:51 +0200 Subject: [PATCH 088/142] formatting --- .../src/test/scala-3/io/sphere/mongo/SerializationTest.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/SerializationTest.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/SerializationTest.scala index f5ac3e58..c3e175e4 100644 --- a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/SerializationTest.scala +++ b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/SerializationTest.scala @@ -179,8 +179,8 @@ class SerializationTest extends AnyWordSpec with Matchers { val colors = List(Color.Red, Color.Yellow, Color.Blue) val roundTripColors = colors.map(mongo.toMongoValue).map(mongo.fromMongoValue) - colors must be (roundTripColors) - + colors must be(roundTripColors) + // mongo java driver knows how to encode/decode Strings val serializedObject = mongo.toMongoValue(Color.Red).asInstanceOf[String] serializedObject must be("Red") From fe782164dfc718b5b3a0ca1397fc84d88381b188 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 25 Apr 2025 14:33:21 +0200 Subject: [PATCH 089/142] Move mongoEnum to its original package --- .../scala-3/io/sphere/mongo/generic/AnnotationReader.scala | 6 ------ .../src/main/scala-3/io/sphere/mongo/generic/generic.scala | 6 ++++++ .../test/scala-3/io/sphere/mongo/SerializationTest.scala | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/AnnotationReader.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/AnnotationReader.scala index bcf32d8f..2a5a4a99 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/AnnotationReader.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/AnnotationReader.scala @@ -37,12 +37,6 @@ case class TraitMetaData( object AnnotationReader { - def mongoEnum(e: Enumeration): MongoFormat[e.Value] = new MongoFormat[e.Value] { - def toMongoValue(a: e.Value): Any = a.toString - - def fromMongoValue(any: Any): e.Value = e.withName(any.asInstanceOf[String]) - } - inline def readTraitMetaData[T]: TraitMetaData = ${ readTraitMetaDataImpl[T] } inline def readCaseClassMetaData[T]: CaseClassMetaData = ${ readCaseClassMetaDataImpl[T] } diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala index 362d426e..c90dc900 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala @@ -6,6 +6,12 @@ import org.bson.BSONObject import scala.compiletime.{erasedValue, error, summonInline} +def mongoEnum(e: Enumeration): MongoFormat[e.Value] = new MongoFormat[e.Value] { + def toMongoValue(a: e.Value): Any = a.toString + + def fromMongoValue(any: Any): e.Value = e.withName(any.asInstanceOf[String]) +} + inline def mongoTypeSwitch[SuperType, SubTypeTuple <: Tuple](): MongoFormat[SuperType] = new MongoFormat[SuperType] { private val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/SerializationTest.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/SerializationTest.scala index c3e175e4..cef6dd0a 100644 --- a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/SerializationTest.scala +++ b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/SerializationTest.scala @@ -175,7 +175,7 @@ class SerializationTest extends AnyWordSpec with Matchers { } "serialize and deserialize enumerations" in { - val mongo: MongoFormat[Color.Value] = AnnotationReader.mongoEnum(Color) + val mongo: MongoFormat[Color.Value] = generic.mongoEnum(Color) val colors = List(Color.Red, Color.Yellow, Color.Blue) val roundTripColors = colors.map(mongo.toMongoValue).map(mongo.fromMongoValue) From 4cfe4b8ac4f91ff9582518ae9ac7f1f4d36f9549 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 25 Apr 2025 16:28:50 +0200 Subject: [PATCH 090/142] Enable enum instances/tests for Scala3 --- .../io.sphere.json/generic/generic.scala | 59 ++++++++++++++++++- .../io/sphere/json/generic/JSONSpec.scala | 42 ++++++------- 2 files changed, 79 insertions(+), 22 deletions(-) diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala index f6a83979..54adf277 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala @@ -1,13 +1,70 @@ package io.sphere.json.generic import cats.data.Validated -import io.sphere.json.{JSON, JSONParseError, JValidation} +import cats.syntax.validated.* +import io.sphere.json.{ + FromJSON, + JSON, + JSONError, + JSONParseError, + JValidation, + ToJSON, + jsonParseError, + toJSON, + toJValue +} import org.json4s.DefaultJsonFormats.given import org.json4s.{JObject, JString, jvalue2monadic, jvalue2readerSyntax} import org.json4s.JsonAST.JValue +import scala.collection.mutable import scala.compiletime.{erasedValue, error, summonInline} +/** Creates a ToJSON instance for an Enumeration type that encodes the `toString` representations of + * the enumeration values. + */ +def toJsonEnum(e: Enumeration): ToJSON[e.Value] = new ToJSON[e.Value] { + def write(a: e.Value): JValue = JString(a.toString) +} + +/** Creates a FromJSON instance for an Enumeration type that encodes the `toString` representations + * of the enumeration values. + */ +def fromJsonEnum(e: Enumeration): FromJSON[e.Value] = { + // We're using an AnyRefMap for performance, it's not for mutability. + val validRepresentations = mutable.AnyRefMap( + e.values.iterator.map { value => + value.toString -> value.validNel[JSONError] + }.toSeq: _* + ) + + val allowedValues = e.values.mkString("'", "','", "'") + + new FromJSON[e.Value] { + override def read(json: JValue): JValidation[e.Value] = + json match { + case JString(string) => + validRepresentations.getOrElse( + string, + jsonParseError( + "Invalid enum value: '%s'. Expected one of: %s".format(string, allowedValues))) + case _ => jsonParseError("JSON String expected.") + } + } +} + +// This can be used instead of deriveJSON +def jsonEnum(e: Enumeration): JSON[e.Value] = { + val toJson = toJsonEnum(e) + val fromJson = fromJsonEnum(e) + + new JSON[e.Value] { + override def read(jval: JValue): JValidation[e.Value] = fromJson.read(jval) + + override def write(value: e.Value): JValue = toJson.write(value) + } +} + inline def jsonTypeSwitch[SuperType, SubTypeTuple <: Tuple](): JSON[SuperType] = new JSON[SuperType] { private val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] diff --git a/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala b/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala index 8a764031..a978f9e6 100644 --- a/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala +++ b/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala @@ -202,17 +202,27 @@ class JSONSpec extends AnyFunSpec with Matchers { fromJSON[Mixed](toJSON(m)) must equal(Valid(m)) } } -// -// it("must provide derived instances for scala.Enumeration") { -// import io.sphere.json.generic.JSON.derived -// implicit val scalaEnumJSON: JSON[JSONSpec.ScalaEnum.Value] = deriveJSON[ScalaEnum.Value] -// ScalaEnum.values.foreach { v => -// val json = s"""[${toJSON(v)}]""" -// withClue(json) { -// fromJSON[Seq[ScalaEnum.Value]](json) must equal(Valid(Seq(v))) -// } -// } -// } + it("must provide instances for scala.Enumeration") { + implicit val toScalaEnumJSON = toJsonEnum(ScalaEnum) + implicit val fromScalaEnumJSON = fromJsonEnum(ScalaEnum) + ScalaEnum.values.foreach { v => + val json = s"""[${toJSON(v)}]""" + withClue(json) { + fromJSON[Seq[ScalaEnum.Value]](json) must equal(Valid(Seq(v))) + } + } + } + + it("must provide instances for scala.Enumeration through jsonEnum") { + // We dropped support for deriveJSON, because there was no derivation anyway, the derivation just called these methods + implicit val scalaEnumJSON: JSON[JSONSpec.ScalaEnum.Value] = jsonEnum(ScalaEnum) + ScalaEnum.values.foreach { v => + val json = s"""[${toJSON(v)}]""" + withClue(json) { + fromJSON[Seq[ScalaEnum.Value]](json) must equal(Valid(Seq(v))) + } + } + } // // it("must handle subclasses correctly in `jsonTypeSwitch`") { // implicit val jsonImpl = TestSubjectBase.json @@ -307,16 +317,6 @@ class JSONSpec extends AnyFunSpec with Matchers { // } // } // -// it("must provide derived instances for scala.Enumeration") { -// implicit val toScalaEnumJSON = toJsonEnum(ScalaEnum) -// implicit val fromScalaEnumJSON = fromJsonEnum(ScalaEnum) -// ScalaEnum.values.foreach { v => -// val json = s"""[${toJSON(v)}]""" -// withClue(json) { -// fromJSON[Seq[ScalaEnum.Value]](json) must equal(Valid(Seq(v))) -// } -// } -// } // // it("must handle subclasses correctly in `jsonTypeSwitch`") { // // ToJSON From a2bbfac61dc4a7253f564026395918a8e82577c4 Mon Sep 17 00:00:00 2001 From: Marcelo Gomes Date: Fri, 25 Apr 2025 16:31:45 +0200 Subject: [PATCH 091/142] Fix warnings --- .../scala-3/io.sphere.json/FromJSON.scala | 10 +- .../main/scala-3/io.sphere.json/JSON.scala | 11 +- .../main/scala-3/io.sphere.json/ToJSON.scala | 1 - .../generic/DeriveFromJSON.scala | 116 +++++++++--------- .../generic/DeriveSingleton.scala | 77 ++++++------ .../io.sphere.json/generic/DeriveToJSON.scala | 75 +++++------ .../io.sphere.json/generic/generic.scala | 59 +++++---- .../io/sphere/json/MoneyMarshallingSpec.scala | 12 +- .../io/sphere/mongo/format/MongoFormat.scala | 14 ++- .../io/sphere/mongo/generic/generic.scala | 41 +++---- .../mongo/format/DefaultMongoFormats.scala | 8 +- .../format/BaseMoneyMongoFormatTest.scala | 2 +- .../format/DefaultMongoFormatsTest.scala | 2 +- .../sphere/mongo/generic/MongoKeySpec.scala | 2 +- .../sphere/mongo/generic/MongoKeySpec.scala | 2 +- util/src/test/scala/DomainObjectsGen.scala | 2 +- .../test/scala/HighPrecisionMoneySpec.scala | 8 +- 17 files changed, 235 insertions(+), 207 deletions(-) diff --git a/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala index 820fc31b..37013977 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala @@ -13,10 +13,16 @@ trait FromJSON[A] extends Serializable { } object FromJSON extends FromJSONInstances with FromJSONCatsInstances with generic.DeriveFromJSON { + val emptyFieldsSet: Set[String] = Set.empty inline def apply[A: JSON]: FromJSON[A] = summon[FromJSON[A]] + inline def apply[A](using instance: FromJSON[A]): FromJSON[A] = instance - private[FromJSON] val emptyFieldsSet: Set[String] = Set.empty + def instance[A]( + readFn: JValue => JValidation[A], + fieldSet: Set[String] = emptyFieldsSet): FromJSON[A] = new { - inline def apply[A](using instance: FromJSON[A]): FromJSON[A] = instance + override def read(jval: JValue): JValidation[A] = readFn(jval) + override val fields: Set[String] = fieldSet + } } diff --git a/json/json-core/src/main/scala-3/io.sphere.json/JSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/JSON.scala index 201b1d85..94924995 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/JSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/JSON.scala @@ -11,9 +11,18 @@ inline def deriveJSON[A](using Mirror.Of[A]): JSON[A] = JSON.derived object JSON extends JSONCatsInstances { inline def apply[A: JSON]: JSON[A] = summon[JSON[A]] - inline given derived[A](using fromJSON: FromJSON[A], toJSON: ToJSON[A]): JSON[A] = instance + def instance[A]( + readFn: JValue => JValidation[A], + writeFn: A => JValue, + fieldSet: Set[String] = FromJSON.emptyFieldsSet): JSON[A] = new { + + override def read(jval: JValue): JValidation[A] = readFn(jval) + override def write(value: A): JValue = writeFn(value) + override val fields: Set[String] = fieldSet + } + private def instance[A](using fromJSON: FromJSON[A], toJSON: ToJSON[A]): JSON[A] = new JSON[A] { override def read(jval: JValue): JValidation[A] = fromJSON.read(jval) diff --git a/json/json-core/src/main/scala-3/io.sphere.json/ToJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/ToJSON.scala index 1ed15304..26123236 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/ToJSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/ToJSON.scala @@ -13,7 +13,6 @@ trait ToJSON[A] extends Serializable { class JSONWriteException(msg: String) extends JSONException(msg) object ToJSON extends ToJSONInstances with ToJSONCatsInstances with generic.DeriveToJSON { - inline def apply[A](implicit instance: ToJSON[A]): ToJSON[A] = instance inline def apply[A: JSON]: ToJSON[A] = summon[ToJSON[A]] diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala index f3710a7b..fe5eb850 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala @@ -26,72 +26,72 @@ trait DeriveFromJSON { case p: Mirror.ProductOf[A] => deriveCaseClass(p) } - inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): FromJSON[A] = - new FromJSON[A] { - private val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] - private val typeHintMap: Map[String, String] = traitMetaData.subtypes.collect { - case (name, classMeta) if classMeta.typeHint.isDefined => - name -> classMeta.typeHint.get - } - private val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) - private val fromJsons: Seq[FromJSON[Any]] = summonFromJsons[mirrorOfSum.MirroredElemTypes] - private val names: Seq[String] = - constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector - .asInstanceOf[Vector[String]] - private val jsonsByNames: Map[String, FromJSON[Any]] = names.zip(fromJsons).toMap - - override def read(jValue: JValue): JValidation[A] = - jValue match { - case jObject: JObject => - val typeName = (jObject \ traitMetaData.typeDiscriminator).as[String] - val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - jsonsByNames(originalTypeName).read(jObject).map(_.asInstanceOf[A]) - case x => - Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$jValue'")) - } + inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): FromJSON[A] = { + val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] + + val typeHintMap: Map[String, String] = traitMetaData.subtypes.flatMap { + case (name, classMeta) if classMeta.typeHint.isDefined => + classMeta.typeHint.map(name -> _) + case _ => + None } - inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): FromJSON[A] = - new FromJSON[A] { - private val caseClassMetaData: CaseClassMetaData = AnnotationReader.readCaseClassMetaData[A] - private val fromJsons: Vector[FromJSON[Any]] = - summonFromJsons[mirrorOfProduct.MirroredElemTypes] - private val fieldsAndJsons: Vector[(Field, FromJSON[Any])] = - caseClassMetaData.fields.zip(fromJsons) - - private val fieldNames: Vector[String] = fieldsAndJsons.flatMap { (field, fromJson) => - if (field.embedded) fromJson.fields.toVector :+ field.name - else Vector(field.name) - } - - override val fields: Set[String] = fieldNames.toSet + val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) + val fromJsons: Seq[FromJSON[Any]] = summonFromJsons[mirrorOfSum.MirroredElemTypes] - override def read(jValue: JValue): JValidation[A] = - jValue match { - case jObject: JObject => - println(s"deriveCaseClass ${fields}") - for { - fieldsAsAList <- fieldsAndJsons - .map((field, fromJson) => readField(field, fromJson, jObject)) - .sequence - fieldsAsTuple = Tuple.fromArray(fieldsAsAList.toArray) + val names: Seq[String] = + constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector + .asInstanceOf[Vector[String]] - } yield mirrorOfProduct.fromTuple( - fieldsAsTuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) + val jsonsByNames: Map[String, FromJSON[Any]] = names.zip(fromJsons).toMap - case x => - Validated.invalidNel(JSONParseError(s"JSON object expected. $x")) - } - - private def readField( - field: Field, - fromJson: FromJSON[Any], - jObject: JObject): JValidation[Any] = - if (field.embedded) fromJson.read(jObject) - else io.sphere.json.field(field.fieldName, field.defaultArgument)(jObject)(fromJson) + FromJSON.instance( + readFn = { + case jObject: JObject => + val typeName = (jObject \ traitMetaData.typeDiscriminator).as[String] + val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) + jsonsByNames(originalTypeName).read(jObject).map(_.asInstanceOf[A]) + case x => + Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$x'")) + } + ) + } + + inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): FromJSON[A] = { + val caseClassMetaData: CaseClassMetaData = AnnotationReader.readCaseClassMetaData[A] + val fromJsons: Vector[FromJSON[Any]] = + summonFromJsons[mirrorOfProduct.MirroredElemTypes] + val fieldsAndJsons: Vector[(Field, FromJSON[Any])] = + caseClassMetaData.fields.zip(fromJsons) + + val fieldNames: Vector[String] = fieldsAndJsons.flatMap { (field, fromJson) => + if (field.embedded) fromJson.fields.toVector :+ field.name + else Vector(field.name) } + def readField(field: Field, fromJson: FromJSON[Any], jObject: JObject): JValidation[Any] = + if (field.embedded) fromJson.read(jObject) + else io.sphere.json.field(field.fieldName, field.defaultArgument)(jObject)(fromJson) + + FromJSON.instance( + readFn = { + case jObject: JObject => + for { + fieldsAsAList <- + fieldsAndJsons.traverse((field, fromJson) => readField(field, fromJson, jObject)) + + fieldsAsTuple = Tuple.fromArray(fieldsAsAList.toArray) + } yield mirrorOfProduct.fromTuple( + fieldsAsTuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) + + case x => + Validated.invalidNel(JSONParseError(s"JSON object expected. $x")) + }, + fieldSet = fieldNames.toSet + ) + } + inline private def summonFromJsons[T <: Tuple]: Vector[FromJSON[Any]] = inline erasedValue[T] match { case _: EmptyTuple => Vector.empty diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala index 2b65c923..4d13d279 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala @@ -22,54 +22,57 @@ object DeriveSingleton { case p: Mirror.ProductOf[A] => deriveObject(p) } - inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): JSON[A] = - new JSON[A] { - private val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] - private val typeHintMap: Map[String, String] = traitMetaData.subtypes.collect { - case (name, classMeta) if classMeta.typeHint.isDefined => - name -> classMeta.typeHint.get - } - private val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) - private val jsons: Seq[JSON[Any]] = summonFormatters[mirrorOfSum.MirroredElemTypes] - private val names: Seq[String] = - constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector - .asInstanceOf[Vector[String]] - private val jsonsByNames: Map[String, JSON[Any]] = names.zip(jsons).toMap - - override def read(jValue: JValue): JValidation[A] = - jValue match { - case JString(typeName) => - val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - jsonsByNames.get(originalTypeName) match { - case Some(json) => - json.read(JNull).map(_.asInstanceOf[A]) - case None => - Validated.invalidNel(JSONParseError(s"'$typeName' is not a valid value")) - } - - case x => - Validated.invalidNel(JSONParseError(s"JSON string expected. Got >>> $jValue")) - } - - override def write(value: A): JValue = { + inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): JSON[A] = { + val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] + + val typeHintMap: Map[String, String] = traitMetaData.subtypes.flatMap { + case (name, classMeta) if classMeta.typeHint.isDefined => + classMeta.typeHint.map(name -> _) + case _ => + None + } + + val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) + val jsons: Seq[JSON[Any]] = summonFormatters[mirrorOfSum.MirroredElemTypes] + + val names: Seq[String] = + constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector + .asInstanceOf[Vector[String]] + + val jsonsByNames: Map[String, JSON[Any]] = names.zip(jsons).toMap + + JSON.instance( + readFn = { + case JString(typeName) => + val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) + jsonsByNames.get(originalTypeName) match { + case Some(json) => + json.read(JNull).map(_.asInstanceOf[A]) + case None => + Validated.invalidNel(JSONParseError(s"'$typeName' is not a valid value")) + } + + case x => + Validated.invalidNel(JSONParseError(s"JSON string expected. Got >>> $x")) + }, + writeFn = { value => val originalTypeName = value.asInstanceOf[Product].productPrefix val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) JString(typeName) } - - } + ) + } inline private def deriveObject[A](mirrorOfProduct: Mirror.ProductOf[A]): JSON[A] = - new JSON[A] { - override def write(value: A): JValue = ??? // This is already taken care of in `deriveTrait` - - override def read(jValue: JValue): JValidation[A] = { + JSON.instance( + writeFn = { _ => ??? }, // This is already taken care of in `deriveTrait` + readFn = { _ => // Just create the object instance, no need to do anything else val tuple = Tuple.fromArray(Array.empty[Any]) val obj = mirrorOfProduct.fromTuple(tuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) Validated.Valid(obj) } - } + ) inline private def summonFormatters[T <: Tuple]: Vector[JSON[Any]] = inline erasedValue[T] match { diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala index d9a9b099..0e354d79 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala @@ -27,46 +27,49 @@ trait DeriveToJSON { case p: Mirror.ProductOf[A] => deriveCaseClass(p) } - inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): ToJSON[A] = - new ToJSON[A] { - private val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] - private val typeHintMap: Map[String, String] = traitMetaData.subtypes.collect { - case (name, classMeta) if classMeta.typeHint.isDefined => - name -> classMeta.typeHint.get - } - private val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) - private val jsons: Seq[ToJSON[Any]] = summonToJson[mirrorOfSum.MirroredElemTypes] - private val names: Seq[String] = - constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector - .asInstanceOf[Vector[String]] - private val jsonsByNames: Map[String, ToJSON[Any]] = names.zip(jsons).toMap - - override def write(value: A): JValue = { - // we never get a trait here, only classes, it's safe to assume Product - val originalTypeName = value.asInstanceOf[Product].productPrefix - val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) - val json = jsonsByNames(originalTypeName).write(value).asInstanceOf[JObject] - val typeDiscriminator = traitMetaData.typeDiscriminator -> JString(typeName) - JObject(typeDiscriminator :: json.obj) - } + inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): ToJSON[A] = { + val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] + val typeHintMap: Map[String, String] = traitMetaData.subtypes.flatMap { + case (name, classMeta) if classMeta.typeHint.isDefined => + classMeta.typeHint.map(name -> _) + case _ => + None } - inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): ToJSON[A] = - new ToJSON[A] { - private val caseClassMetaData: CaseClassMetaData = AnnotationReader.readCaseClassMetaData[A] - private val toJsons: Vector[ToJSON[Any]] = summonToJson[mirrorOfProduct.MirroredElemTypes] - - override def write(value: A): JValue = { - val caseClassFields = value.asInstanceOf[Product].productIterator - toJsons - .zip(caseClassFields) - .zip(caseClassMetaData.fields) - .foldLeft[JValue](JObject()) { case (jObject, ((toJson, fieldValue), field)) => - addField(jObject.asInstanceOf[JObject], field, toJson.write(fieldValue)) - } - } + val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) + val jsons: Seq[ToJSON[Any]] = summonToJson[mirrorOfSum.MirroredElemTypes] + + val names: Seq[String] = + constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector + .asInstanceOf[Vector[String]] + + val jsonsByNames: Map[String, ToJSON[Any]] = names.zip(jsons).toMap + + ToJSON.instance { value => + // we never get a trait here, only classes, it's safe to assume Product + val originalTypeName = value.asInstanceOf[Product].productPrefix + val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) + val json = jsonsByNames(originalTypeName).write(value).asInstanceOf[JObject] + val typeDiscriminator = traitMetaData.typeDiscriminator -> JString(typeName) + JObject(typeDiscriminator :: json.obj) + } + } + + inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): ToJSON[A] = { + val caseClassMetaData: CaseClassMetaData = AnnotationReader.readCaseClassMetaData[A] + val toJsons: Vector[ToJSON[Any]] = summonToJson[mirrorOfProduct.MirroredElemTypes] + + ToJSON.instance { value => + val caseClassFields = value.asInstanceOf[Product].productIterator + toJsons.iterator + .zip(caseClassFields) + .zip(caseClassMetaData.fields) + .foldLeft[JValue](JObject()) { case (jObject, ((toJson, fieldValue), field)) => + addField(jObject.asInstanceOf[JObject], field, toJson.write(fieldValue)) + } } + } inline private def summonToJson[T <: Tuple]: Vector[ToJSON[Any]] = inline erasedValue[T] match { diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala index 54adf277..c42a0767 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala @@ -65,27 +65,27 @@ def jsonEnum(e: Enumeration): JSON[e.Value] = { } } -inline def jsonTypeSwitch[SuperType, SubTypeTuple <: Tuple](): JSON[SuperType] = - new JSON[SuperType] { - private val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] - private val typeHintMap = traitMetaData.subTypeTypeHints - private val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) - private val formattersAndMetaData: Vector[(TraitMetaData, JSON[Any])] = - summonFormatters[SubTypeTuple]() +inline def jsonTypeSwitch[SuperType, SubTypeTuple <: Tuple](): JSON[SuperType] = { + val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] + val typeHintMap = traitMetaData.subTypeTypeHints + val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) + val formattersAndMetaData: Vector[(TraitMetaData, JSON[Any])] = summonFormatters[SubTypeTuple]() - // Separate Trait formatters from CaseClass formatters, so we can avoid adding the typeDiscriminator twice - private val (traitFormatterList, caseClassFormatterList) = - formattersAndMetaData.partitionMap { (meta, formatter) => - if (meta.isTrait) - Left(meta.subtypes.map(_._2.name -> formatter)) - else - Right(meta.top.name -> formatter) - } - val traitFormatters = traitFormatterList.flatten.toMap - val caseClassFormatters = caseClassFormatterList.toMap - val allFormattersByTypeName = traitFormatters ++ caseClassFormatters + // Separate Trait formatters from CaseClass formatters, so we can avoid adding the typeDiscriminator twice + val (traitFormatterList, caseClassFormatterList) = + formattersAndMetaData.partitionMap { (meta, formatter) => + if (meta.isTrait) + Left(meta.subtypes.map(_._2.name -> formatter)) + else + Right(meta.top.name -> formatter) + } - override def write(a: SuperType): JValue = { + val traitFormatters = traitFormatterList.flatten.toMap + val caseClassFormatters = caseClassFormatterList.toMap + val allFormattersByTypeName = traitFormatters ++ caseClassFormatters + + JSON.instance( + writeFn = { a => val originalTypeName = a.asInstanceOf[Product].productPrefix val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) val traitFormatterOpt = traitFormatters.get(originalTypeName) @@ -96,18 +96,17 @@ inline def jsonTypeSwitch[SuperType, SubTypeTuple <: Tuple](): JSON[SuperType] = val typeDiscriminator = traitMetaData.typeDiscriminator -> JString(typeName) JObject(typeDiscriminator :: json.obj) } + }, + readFn = { + case jObject: JObject => + val typeName = (jObject \ traitMetaData.typeDiscriminator).as[String] + val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) + allFormattersByTypeName(originalTypeName).read(jObject).map(_.asInstanceOf[SuperType]) + case x => + Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$x'")) } - - override def read(jValue: JValue): JValidation[SuperType] = - jValue match { - case jObject: JObject => - val typeName = (jObject \ traitMetaData.typeDiscriminator).as[String] - val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - allFormattersByTypeName(originalTypeName).read(jObject).map(_.asInstanceOf[SuperType]) - case x => - Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$jValue'")) - } - } + ) +} inline private def summonFormatters[T <: Tuple]( acc: Vector[(TraitMetaData, JSON[Any])] = Vector.empty): Vector[(TraitMetaData, JSON[Any])] = diff --git a/json/json-core/src/test/scala/io/sphere/json/MoneyMarshallingSpec.scala b/json/json-core/src/test/scala/io/sphere/json/MoneyMarshallingSpec.scala index fae40bd6..048971d5 100644 --- a/json/json-core/src/test/scala/io/sphere/json/MoneyMarshallingSpec.scala +++ b/json/json-core/src/test/scala/io/sphere/json/MoneyMarshallingSpec.scala @@ -14,7 +14,7 @@ class MoneyMarshallingSpec extends AnyWordSpec with Matchers { val money = Money.EUR(34.56) val jsonAst = toJValue(money) val jsonAsString = compactJson(jsonAst) - val Valid(readAst) = parseJSON(jsonAsString) + val Valid(readAst) = parseJSON(jsonAsString): @unchecked jsonAst should equal(readAst) } @@ -52,9 +52,9 @@ class MoneyMarshallingSpec extends AnyWordSpec with Matchers { val money = HighPrecisionMoney.fromDecimalAmount(34.123456, 6, Currency.getInstance("EUR")) val jsonAst = toJValue(money) val jsonAsString = compactJson(jsonAst) - val Valid(readAst) = parseJSON(jsonAsString) - val Valid(decodedMoney) = fromJSON[HighPrecisionMoney](jsonAsString) - val Valid(decodedBaseMoney) = fromJSON[BaseMoney](jsonAsString) + val Valid(readAst) = parseJSON(jsonAsString): @unchecked + val Valid(decodedMoney) = fromJSON[HighPrecisionMoney](jsonAsString): @unchecked + val Valid(decodedBaseMoney) = fromJSON[BaseMoney](jsonAsString): @unchecked jsonAst should equal(readAst) decodedMoney should equal(money) @@ -85,9 +85,9 @@ class MoneyMarshallingSpec extends AnyWordSpec with Matchers { "centAmount": 1, "fractionDigits": 4 } - """) + """): @unchecked - val Valid(parsed) = fromJValue[BaseMoney](json) + val Valid(parsed) = fromJValue[BaseMoney](json): @unchecked toJValue(parsed) should be(json) } diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala index dd8091ed..80bc9d7c 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala @@ -24,12 +24,21 @@ trait MongoFormat[A] extends Serializable { inline def deriveMongoFormat[A](using Mirror.Of[A]): MongoFormat[A] = MongoFormat.derived object MongoFormat { - inline def apply[A: MongoFormat]: MongoFormat[A] = summon - private val emptyFields: Set[String] = Set.empty + inline def apply[A: MongoFormat]: MongoFormat[A] = summon inline given derived[A](using Mirror.Of[A]): MongoFormat[A] = Derivation.derived + def instance[A]( + fromFn: Any => A, + toFn: A => Any, + fieldSet: Set[String] = emptyFields): MongoFormat[A] = new { + + override def toMongoValue(a: A): Any = toFn(a) + override def fromMongoValue(mongoType: Any): A = fromFn(mongoType) + override val fields: Set[String] = fieldSet + } + private def addField(bson: BasicDBObject, field: Field, mongoType: Any): Unit = if (!field.ignored) mongoType match { @@ -41,7 +50,6 @@ object MongoFormat { } private object Derivation { - import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} inline def derived[A](using m: Mirror.Of[A]): MongoFormat[A] = diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala index c90dc900..24d38941 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala @@ -12,34 +12,33 @@ def mongoEnum(e: Enumeration): MongoFormat[e.Value] = new MongoFormat[e.Value] { def fromMongoValue(any: Any): e.Value = e.withName(any.asInstanceOf[String]) } -inline def mongoTypeSwitch[SuperType, SubTypeTuple <: Tuple](): MongoFormat[SuperType] = - new MongoFormat[SuperType] { - private val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] - private val typeHintMap = traitMetaData.subTypeTypeHints - private val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) - private val formatters: Vector[MongoFormat[Any]] = summonFormatters[SubTypeTuple]() - private val names = summonMetaData[SubTypeTuple]().map(_.name) - private val formattersByTypeName = names.zip(formatters).toMap - - override def toMongoValue(a: SuperType): Any = { +inline def mongoTypeSwitch[SuperType, SubTypeTuple <: Tuple](): MongoFormat[SuperType] = { + val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] + val typeHintMap = traitMetaData.subTypeTypeHints + val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) + val formatters: Vector[MongoFormat[Any]] = summonFormatters[SubTypeTuple]() + val names = summonMetaData[SubTypeTuple]().map(_.name) + val formattersByTypeName = names.zip(formatters).toMap + + MongoFormat.instance( + toFn = { a => val originalTypeName = a.asInstanceOf[Product].productPrefix val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) val bson = formattersByTypeName(originalTypeName).toMongoValue(a).asInstanceOf[BasicDBObject] bson.put(traitMetaData.typeDiscriminator, typeName) bson + }, + fromFn = { + case bson: BasicDBObject => + val typeName = bson.get(traitMetaData.typeDiscriminator).asInstanceOf[String] + val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) + formattersByTypeName(originalTypeName).fromMongoValue(bson).asInstanceOf[SuperType] + case x => + throw new Exception(s"BsonObject is expected for a Trait subtype, instead got $x") } - - override def fromMongoValue(bson: Any): SuperType = - bson match { - case bson: BasicDBObject => - val typeName = bson.get(traitMetaData.typeDiscriminator).asInstanceOf[String] - val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - formattersByTypeName(originalTypeName).fromMongoValue(bson).asInstanceOf[SuperType] - case x => - throw new Exception(s"BsonObject is expected for a Trait subtype, instead got $x") - } - } + ) +} private def findTypeValue(dbo: BSONObject, typeField: String): Option[String] = Option(dbo.get(typeField)).map(_.toString) diff --git a/mongo/mongo-core/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala b/mongo/mongo-core/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala index 2c30f5ab..06ce679a 100644 --- a/mongo/mongo-core/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala +++ b/mongo/mongo-core/src/main/scala/io/sphere/mongo/format/DefaultMongoFormats.scala @@ -49,7 +49,7 @@ trait DefaultMongoFormats { implicit def optionFormat[@specialized A](implicit f: MongoFormat[A]): MongoFormat[Option[A]] = new MongoFormat[Option[A]] { - import scala.collection.JavaConverters._ + import scala.jdk.CollectionConverters._ override def toMongoValue(a: Option[A]) = a match { case Some(aa) => f.toMongoValue(aa) case None => MongoNothing @@ -77,7 +77,7 @@ trait DefaultMongoFormats { implicit def vecFormat[@specialized A](implicit f: MongoFormat[A]): MongoFormat[Vector[A]] = new MongoFormat[Vector[A]] { - import scala.collection.JavaConverters._ + import scala.jdk.CollectionConverters._ override def toMongoValue(a: Vector[A]): BasicBSONList = { val m = new BasicBSONList() if (a.nonEmpty) m.addAll(a.map(f.toMongoValue(_).asInstanceOf[AnyRef]).asJavaCollection) @@ -102,7 +102,7 @@ trait DefaultMongoFormats { implicit def listFormat[@specialized A](implicit f: MongoFormat[A]): MongoFormat[List[A]] = new MongoFormat[List[A]] { - import scala.collection.JavaConverters._ + import scala.jdk.CollectionConverters._ override def toMongoValue(a: List[A]): BasicBSONList = { val m = new BasicBSONList() if (a.nonEmpty) m.addAll(a.map(f.toMongoValue(_).asInstanceOf[AnyRef]).asJavaCollection) @@ -127,7 +127,7 @@ trait DefaultMongoFormats { implicit def setFormat[@specialized A](implicit f: MongoFormat[A]): MongoFormat[Set[A]] = new MongoFormat[Set[A]] { - import scala.collection.JavaConverters._ + import scala.jdk.CollectionConverters._ override def toMongoValue(a: Set[A]): BasicBSONList = { val m = new BasicBSONList() if (a.nonEmpty) m.addAll(a.map(f.toMongoValue(_).asInstanceOf[AnyRef]).asJavaCollection) diff --git a/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala b/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala index 27ab0fd0..9638fd06 100644 --- a/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala +++ b/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/BaseMoneyMongoFormatTest.scala @@ -9,7 +9,7 @@ import org.bson.BSONObject import org.scalatest.wordspec.AnyWordSpec import org.scalatest.matchers.should.Matchers -import scala.collection.JavaConverters._ +import scala.jdk.CollectionConverters._ class BaseMoneyMongoFormatTest extends AnyWordSpec with Matchers { diff --git a/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/DefaultMongoFormatsTest.scala b/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/DefaultMongoFormatsTest.scala index f03efc81..a29bf5c7 100644 --- a/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/DefaultMongoFormatsTest.scala +++ b/mongo/mongo-core/src/test/scala/io/sphere/mongo/format/DefaultMongoFormatsTest.scala @@ -12,7 +12,7 @@ import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks -import scala.collection.JavaConverters._ +import scala.jdk.CollectionConverters._ object DefaultMongoFormatsTest { case class User(name: String) diff --git a/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala b/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala index 3392124b..2edd6d21 100644 --- a/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala +++ b/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala @@ -6,7 +6,7 @@ import org.bson.BSONObject import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec -import scala.collection.JavaConverters._ +import scala.jdk.CollectionConverters._ class MongoKeySpec extends AnyWordSpec with Matchers { import MongoKeySpec._ diff --git a/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala b/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala index 2f001de0..330dac5b 100644 --- a/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala +++ b/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala @@ -6,7 +6,7 @@ import org.bson.BSONObject import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec -import scala.collection.JavaConverters._ +import scala.jdk.CollectionConverters._ class MongoKeySpec extends AnyWordSpec with Matchers { import MongoKeySpec._ diff --git a/util/src/test/scala/DomainObjectsGen.scala b/util/src/test/scala/DomainObjectsGen.scala index 97536bfd..b654f020 100644 --- a/util/src/test/scala/DomainObjectsGen.scala +++ b/util/src/test/scala/DomainObjectsGen.scala @@ -4,7 +4,7 @@ import java.util.Currency import org.scalacheck.Gen -import scala.collection.JavaConverters._ +import scala.jdk.CollectionConverters._ object DomainObjectsGen { diff --git a/util/src/test/scala/HighPrecisionMoneySpec.scala b/util/src/test/scala/HighPrecisionMoneySpec.scala index 1b80016b..386467bb 100644 --- a/util/src/test/scala/HighPrecisionMoneySpec.scala +++ b/util/src/test/scala/HighPrecisionMoneySpec.scala @@ -168,20 +168,22 @@ class HighPrecisionMoneySpec extends AnyFunSpec with Matchers with ScalaCheckDri } it("should validate fractionDigits (min)") { - val Invalid(errors) = HighPrecisionMoney.fromPreciseAmount(123456L, 1, Euro, None) + val Invalid(errors) = HighPrecisionMoney.fromPreciseAmount(123456L, 1, Euro, None): @unchecked errors.toList must be( List("fractionDigits must be > 2 (default fraction digits defined by currency EUR).")) } it("should validate fractionDigits (max)") { - val Invalid(errors) = HighPrecisionMoney.fromPreciseAmount(123456L, 100, Euro, None) + val Invalid(errors) = + HighPrecisionMoney.fromPreciseAmount(123456L, 100, Euro, None): @unchecked errors.toList must be(List("fractionDigits must be <= 20.")) } it("should validate centAmount") { - val Invalid(errors) = HighPrecisionMoney.fromPreciseAmount(123456L, 4, Euro, Some(1)) + val Invalid(errors) = + HighPrecisionMoney.fromPreciseAmount(123456L, 4, Euro, Some(1)): @unchecked errors.toList must be( List( From c39a9d17627bf27e5f0abc6e7f4d4f153f669f72 Mon Sep 17 00:00:00 2001 From: Marcelo Gomes Date: Fri, 25 Apr 2025 17:19:21 +0200 Subject: [PATCH 092/142] Remove support for scala 2.12 --- .github/workflows/ci.yml | 16 +++------------- build.sbt | 24 +++++++++++------------- 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb767ece..a16b6658 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,9 +9,9 @@ name: Continuous Integration on: pull_request: - branches: ['**'] + branches: ["**"] push: - branches: ['**'] + branches: ["**"] tags: [v*] env: @@ -24,7 +24,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - scala: [2.12.20, 2.13.16, 3.3.5] + scala: [2.13.16, 3.3.5] java: [temurin@21] runs-on: ${{ matrix.os }} steps: @@ -94,16 +94,6 @@ jobs: - name: Setup sbt uses: sbt/setup-sbt@v1 - - name: Download target directories (2.12.20) - uses: actions/download-artifact@v4 - with: - name: target-${{ matrix.os }}-2.12.20-${{ matrix.java }} - - - name: Inflate target directories (2.12.20) - run: | - tar xf targets.tar - rm targets.tar - - name: Download target directories (2.13.16) uses: actions/download-artifact@v4 with: diff --git a/build.sbt b/build.sbt index b1c26441..5c9dbaca 100644 --- a/build.sbt +++ b/build.sbt @@ -1,11 +1,10 @@ import pl.project13.scala.sbt.JmhPlugin -lazy val scala212 = "2.12.20" lazy val scala213 = "2.13.16" lazy val scala3 = "3.3.5" // sbt-github-actions needs configuration in `ThisBuild` -ThisBuild / crossScalaVersions := Seq(scala212, scala213, scala3) +ThisBuild / crossScalaVersions := Seq(scala213, scala3) ThisBuild / scalaVersion := scala213 ThisBuild / githubWorkflowPublishTargetBranches := List() ThisBuild / githubWorkflowJavaVersions := List(JavaSpec.temurin("21")) @@ -80,8 +79,7 @@ lazy val standardSettings = Defaults.coreDefaultSettings ++ Seq( javacOptions ++= Seq("-deprecation", "-Xlint:unchecked"), // targets Java 8 bytecode (scalac & javac) scalacOptions ++= { - if (scalaVersion.value.startsWith("2.12")) Seq.empty - else if (scalaVersion.value.startsWith("3")) Seq("-noindent") + if (scalaVersion.value.startsWith("3")) Seq("-noindent") else Seq("-target", "8") }, ThisBuild / javacOptions ++= Seq("-source", "8", "-target", "8"), @@ -119,21 +117,21 @@ lazy val `sphere-util` = project .in(file("./util")) .settings(standardSettings: _*) .settings(scalaVersion := scala3) - .settings(crossScalaVersions := Seq(scala212, scala213, scala3)) + .settings(crossScalaVersions := Seq(scala213, scala3)) .settings(homepage := Some(url("https://github.com/commercetools/sphere-scala-libs/README.md"))) lazy val `sphere-json-core` = project .in(file("./json/json-core")) .settings(standardSettings: _*) .settings(scalaVersion := scala3) - .settings(crossScalaVersions := Seq(scala212, scala213, scala3)) + .settings(crossScalaVersions := Seq(scala213, scala3)) .dependsOn(`sphere-util`) lazy val `sphere-mongo-core` = project .in(file("./mongo/mongo-core")) .settings(standardSettings: _*) .settings(scalaVersion := scala3) - .settings(crossScalaVersions := Seq(scala212, scala213, scala3)) + .settings(crossScalaVersions := Seq(scala213, scala3)) .dependsOn(`sphere-util`) // Scala 2 modules @@ -142,7 +140,7 @@ lazy val `sphere-json-derivation` = project .in(file("./json/json-derivation")) .settings(standardSettings: _*) .settings(Fmpp.settings: _*) - .settings(crossScalaVersions := Seq(scala212, scala213)) + .settings(crossScalaVersions := Seq(scala213)) .dependsOn(`sphere-json-core`) lazy val `sphere-json` = project @@ -150,20 +148,20 @@ lazy val `sphere-json` = project .settings(standardSettings: _*) .settings(homepage := Some( url("https://github.com/commercetools/sphere-scala-libs/blob/master/json/README.md"))) - .settings(crossScalaVersions := Seq(scala212, scala213)) + .settings(crossScalaVersions := Seq(scala213)) .dependsOn(`sphere-json-core`, `sphere-json-derivation`) lazy val `sphere-mongo-derivation` = project .in(file("./mongo/mongo-derivation")) .settings(standardSettings: _*) .settings(Fmpp.settings: _*) - .settings(crossScalaVersions := Seq(scala212, scala213)) + .settings(crossScalaVersions := Seq(scala213)) .dependsOn(`sphere-mongo-core`) lazy val `sphere-mongo-derivation-magnolia` = project .in(file("./mongo/mongo-derivation-magnolia")) .settings(standardSettings: _*) - .settings(crossScalaVersions := Seq(scala212, scala213)) + .settings(crossScalaVersions := Seq(scala213)) .dependsOn(`sphere-mongo-core`) lazy val `sphere-mongo` = project @@ -171,7 +169,7 @@ lazy val `sphere-mongo` = project .settings(standardSettings: _*) .settings(homepage := Some( url("https://github.com/commercetools/sphere-scala-libs/blob/master/mongo/README.md"))) - .settings(crossScalaVersions := Seq(scala212, scala213)) + .settings(crossScalaVersions := Seq(scala213)) .dependsOn(`sphere-mongo-core`, `sphere-mongo-derivation`) // benchmarks @@ -179,6 +177,6 @@ lazy val `sphere-mongo` = project lazy val benchmarks = project .settings(standardSettings: _*) .settings(publishArtifact := false, publish := {}) - .settings(crossScalaVersions := Seq(scala212, scala213)) + .settings(crossScalaVersions := Seq(scala213)) .enablePlugins(JmhPlugin) .dependsOn(`sphere-util`, `sphere-json`, `sphere-mongo`) From d7237a0bb4036b2d870a427d49aa28f9456d352f Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 25 Apr 2025 17:31:18 +0200 Subject: [PATCH 093/142] Trying to fix pipeline --- .github/workflows/ci.yml | 4 +-- .../scala-3/io.sphere.json/FromJSON.scala | 1 - .../io/sphere/json/generic/JSONSpec.scala | 31 +++++++++---------- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a16b6658..e6f0de1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,9 +9,9 @@ name: Continuous Integration on: pull_request: - branches: ["**"] + branches: ['**'] push: - branches: ["**"] + branches: ['**'] tags: [v*] env: diff --git a/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala index 37013977..1f0b4a5d 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala @@ -15,7 +15,6 @@ trait FromJSON[A] extends Serializable { object FromJSON extends FromJSONInstances with FromJSONCatsInstances with generic.DeriveFromJSON { val emptyFieldsSet: Set[String] = Set.empty - inline def apply[A: JSON]: FromJSON[A] = summon[FromJSON[A]] inline def apply[A](using instance: FromJSON[A]): FromJSON[A] = instance def instance[A]( diff --git a/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala b/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala index a978f9e6..d9ea6328 100644 --- a/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala +++ b/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala @@ -141,6 +141,19 @@ class JSONSpec extends AnyFunSpec with Matchers { fromJSON[Project](toJSON(proj)) must equal(Valid(proj)) } + it( + "must provide derived JSON instances for product types (case classes) through FromJSON and ToJSON") { + import JSONSpec.{Milestone, Project} + given ToJSON[Milestone] = ToJSON.derived[Milestone] + given ToJSON[Project] = ToJSON.derived[Project] + given FromJSON[Milestone] = FromJSON.derived[Milestone] + given FromJSON[Project] = FromJSON.derived[Project] + + val proj = + Project(42, "Linux", 7, Milestone("1.0") :: Milestone("2.0") :: Milestone("3.0") :: Nil) + fromJSON[Project](toJSON(proj)) must equal(Valid(proj)) + } + it("must handle empty String") { val Invalid(err) = fromJSON[Int](""): @unchecked err.toList.head mustBe a[JSONParseError] @@ -240,11 +253,8 @@ class JSONSpec extends AnyFunSpec with Matchers { // fromJSON[TestSubjectBase](json) must equal(Valid(testSubject)) // } // } -// // } // -// } -// // describe("ToJSON and FromJSON") { // it("must provide derived JSON instances for sum types") { // // ToJSON @@ -358,20 +368,7 @@ class JSONSpec extends AnyFunSpec with Matchers { // } // // } -// -// it("must provide derived JSON instances for product types (case classes)") { -// import JSONSpec.{Milestone, Project} -// // ToJSON -// implicit val milestoneToJSON = toJsonProduct(Milestone.apply _) -// implicit val projectToJSON = toJsonProduct(Project.apply _) -// // FromJSON -// implicit val milestoneFromJSON = fromJsonProduct(Milestone.apply _) -// implicit val projectFromJSON = fromJsonProduct(Project.apply _) -// -// val proj = -// Project(42, "Linux", 7, Milestone("1.0") :: Milestone("2.0") :: Milestone("3.0") :: Nil) -// fromJSON[Project](toJSON(proj)) must equal(Valid(proj)) -// } + } } From 1fc8ed172dfa3c387329f996af65d23f88354319 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 25 Apr 2025 20:23:29 +0200 Subject: [PATCH 094/142] Turn on more test cases in JSONSpec --- .../scala-3/io.sphere.json/FromJSON.scala | 11 +- .../main/scala-3/io.sphere.json/JSON.scala | 2 + .../generic/DeriveFromJSON.scala | 3 +- .../io.sphere.json/generic/generic.scala | 7 +- .../io/sphere/json/generic/JSONSpec.scala | 131 +++++++++--------- 5 files changed, 88 insertions(+), 66 deletions(-) diff --git a/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala index 1f0b4a5d..80bb08c5 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala @@ -10,18 +10,27 @@ trait FromJSON[A] extends Serializable { /** needed JSON fields - ignored if empty */ val fields: Set[String] = FromJSON.emptyFieldsSet + + /** This is used in the TypeSwitch for cases when there's a nested trait with some type hints on + * the case classes of the nested trait. We somehow need to know about the typehints, with this + * we can propagate them. + */ + val traitTypeHintMap: Map[String, String] = FromJSON.emptyTypeHintMap } object FromJSON extends FromJSONInstances with FromJSONCatsInstances with generic.DeriveFromJSON { val emptyFieldsSet: Set[String] = Set.empty + val emptyTypeHintMap: Map[String, String] = Map.empty inline def apply[A](using instance: FromJSON[A]): FromJSON[A] = instance def instance[A]( readFn: JValue => JValidation[A], - fieldSet: Set[String] = emptyFieldsSet): FromJSON[A] = new { + fieldSet: Set[String] = emptyFieldsSet, + typeHintMap: Map[String, String] = emptyTypeHintMap): FromJSON[A] = new { override def read(jval: JValue): JValidation[A] = readFn(jval) override val fields: Set[String] = fieldSet + override val traitTypeHintMap: Map[String, String] = typeHintMap } } diff --git a/json/json-core/src/main/scala-3/io.sphere.json/JSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/JSON.scala index 94924995..56c80c16 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/JSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/JSON.scala @@ -30,6 +30,8 @@ object JSON extends JSONCatsInstances { override def write(value: A): JValue = toJSON.write(value) override val fields: Set[String] = fromJSON.fields + + override val traitTypeHintMap: Map[String, String] = fromJSON.traitTypeHintMap } } diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala index fe5eb850..997b72a8 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala @@ -54,7 +54,8 @@ trait DeriveFromJSON { case x => Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$x'")) - } + }, + typeHintMap = typeHintMap ) } diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala index c42a0767..15c42b5d 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala @@ -67,8 +67,6 @@ def jsonEnum(e: Enumeration): JSON[e.Value] = { inline def jsonTypeSwitch[SuperType, SubTypeTuple <: Tuple](): JSON[SuperType] = { val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] - val typeHintMap = traitMetaData.subTypeTypeHints - val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) val formattersAndMetaData: Vector[(TraitMetaData, JSON[Any])] = summonFormatters[SubTypeTuple]() // Separate Trait formatters from CaseClass formatters, so we can avoid adding the typeDiscriminator twice @@ -84,6 +82,11 @@ inline def jsonTypeSwitch[SuperType, SubTypeTuple <: Tuple](): JSON[SuperType] = val caseClassFormatters = caseClassFormatterList.toMap val allFormattersByTypeName = traitFormatters ++ caseClassFormatters + val subTypeHints = + traitFormatters.map((_, formatter) => formatter.traitTypeHintMap).reduce(_ ++ _) + val typeHintMap = traitMetaData.subTypeTypeHints ++ subTypeHints + val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) + JSON.instance( writeFn = { a => val originalTypeName = a.asInstanceOf[Product].productPrefix diff --git a/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala b/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala index d9ea6328..bbe6cf38 100644 --- a/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala +++ b/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala @@ -76,6 +76,7 @@ class JSONSpec extends AnyFunSpec with Matchers { JField("name", JString(m.name)) :: JField("date", toJValue(m.date)) :: Nil ) + def read(j: JValue): ValidatedNel[JSONError, Milestone] = j match { case o: JObject => (field[String]("name")(o), field[Option[DateTime]]("date")(o)).mapN(Milestone.apply) @@ -90,6 +91,7 @@ class JSONSpec extends AnyFunSpec with Matchers { JField("milestones", toJValue(p.milestones)) :: Nil ) + def read(jval: JValue): ValidatedNel[JSONError, Project] = jval match { case o: JObject => ( @@ -105,7 +107,8 @@ class JSONSpec extends AnyFunSpec with Matchers { fromJSON[Project](toJSON(proj)) must equal(Valid(proj)) // Now some invalid JSON to test the error accumulation - val wrongTypeJSON = """ + val wrongTypeJSON = + """ { "nr":"1", "name":23, @@ -141,19 +144,6 @@ class JSONSpec extends AnyFunSpec with Matchers { fromJSON[Project](toJSON(proj)) must equal(Valid(proj)) } - it( - "must provide derived JSON instances for product types (case classes) through FromJSON and ToJSON") { - import JSONSpec.{Milestone, Project} - given ToJSON[Milestone] = ToJSON.derived[Milestone] - given ToJSON[Project] = ToJSON.derived[Project] - given FromJSON[Milestone] = FromJSON.derived[Milestone] - given FromJSON[Project] = FromJSON.derived[Project] - - val proj = - Project(42, "Linux", 7, Milestone("1.0") :: Milestone("2.0") :: Milestone("3.0") :: Nil) - fromJSON[Project](toJSON(proj)) must equal(Valid(proj)) - } - it("must handle empty String") { val Invalid(err) = fromJSON[Int](""): @unchecked err.toList.head mustBe a[JSONParseError] @@ -216,8 +206,8 @@ class JSONSpec extends AnyFunSpec with Matchers { } } it("must provide instances for scala.Enumeration") { - implicit val toScalaEnumJSON = toJsonEnum(ScalaEnum) - implicit val fromScalaEnumJSON = fromJsonEnum(ScalaEnum) + // We dropped support for deriveJSON, because there was no derivation anyway, the derivation just called these methods + implicit val scalaEnumJSON: JSON[JSONSpec.ScalaEnum.Value] = jsonEnum(ScalaEnum) ScalaEnum.values.foreach { v => val json = s"""[${toJSON(v)}]""" withClue(json) { @@ -226,9 +216,45 @@ class JSONSpec extends AnyFunSpec with Matchers { } } - it("must provide instances for scala.Enumeration through jsonEnum") { - // We dropped support for deriveJSON, because there was no derivation anyway, the derivation just called these methods - implicit val scalaEnumJSON: JSON[JSONSpec.ScalaEnum.Value] = jsonEnum(ScalaEnum) + it("must handle subclasses correctly in `jsonTypeSwitch`") { + given JSON[TestSubjectBase] = TestSubjectBase.json + + val testSubjects = List[TestSubjectBase]( + TestSubjectConcrete1("testSubject1"), + TestSubjectConcrete2("testSubject2"), + TestSubjectConcrete3("testSubject3"), + TestSubjectConcrete4("testSubject4") + ) + + testSubjects.foreach { testSubject => + val json = toJSON(testSubject) + withClue(json) { + fromJSON[TestSubjectBase](json) must equal(Valid(testSubject)) + } + } + } + } + + describe("ToJSON and FromJSON") { + + it("must provide derived JSON instances for product types (case classes)") { + import JSONSpec.{Milestone, Project} + given ToJSON[Milestone] = ToJSON.derived[Milestone] + + given ToJSON[Project] = ToJSON.derived[Project] + + given FromJSON[Milestone] = FromJSON.derived[Milestone] + + given FromJSON[Project] = FromJSON.derived[Project] + + val proj = + Project(42, "Linux", 7, Milestone("1.0") :: Milestone("2.0") :: Milestone("3.0") :: Nil) + fromJSON[Project](toJSON(proj)) must equal(Valid(proj)) + } + + it("must provide instances for scala.Enumeration") { + implicit val toScalaEnumJSON = toJsonEnum(ScalaEnum) + implicit val fromScalaEnumJSON = fromJsonEnum(ScalaEnum) ScalaEnum.values.foreach { v => val json = s"""[${toJSON(v)}]""" withClue(json) { @@ -236,50 +262,30 @@ class JSONSpec extends AnyFunSpec with Matchers { } } } -// -// it("must handle subclasses correctly in `jsonTypeSwitch`") { -// implicit val jsonImpl = TestSubjectBase.json -// -// val testSubjects = List[TestSubjectBase]( -// TestSubjectConcrete1("testSubject1"), -// TestSubjectConcrete2("testSubject2"), -// TestSubjectConcrete3("testSubject3"), -// TestSubjectConcrete4("testSubject4") -// ) -// -// testSubjects.foreach { testSubject => -// val json = toJSON(testSubject) -// withClue(json) { -// fromJSON[TestSubjectBase](json) must equal(Valid(testSubject)) -// } -// } -// } -// -// describe("ToJSON and FromJSON") { + // it("must provide derived JSON instances for sum types") { // // ToJSON // implicit val birdToJSON = deriveJSON[Bird].write(Bird.apply _) // implicit val dogToJSON = deriveJSON[Dog].write(Dog.apply _) -// implicit val catToJSON = toJsonProduct(Cat.apply _) -// implicit val animalToJSON = toJsonTypeSwitch[Animal, Bird, Dog, Cat](Nil) +// implicit val catToJSON = ToJSON.derived[Cat] +// implicit val animalToJSON = jsonTypeSwitch[Animal, (Bird, Dog, Cat)]() // // FromJSON -// implicit val birdFromJSON = fromJsonProduct(Bird.apply _) -// implicit val dogFromJSON = fromJsonProduct(Dog.apply _) -// implicit val catFromJSON = fromJsonProduct(Cat.apply _) -// implicit val animalFromJSON = fromJsonTypeSwitch[Animal, Bird, Dog, Cat](Nil) +// implicit val birdFromJSON = FromJSON.derived[Bird] +// implicit val dogFromJSON = FromJSON.derived[Dog] +// implicit val catFromJSON = FromJSON.derived[Cat] +// implicit val animalFromJSON = jsonTypeSwitch[Animal, (Bird, Dog, Cat)]() // -// List(Bird("Peewee"), Dog("Hasso"), Cat("Felidae")).foreach { -// a: Animal => -// fromJSON[Animal](toJSON(a)) must equal(Valid(a)) +// List(Bird("Peewee"), Dog("Hasso"), Cat("Felidae")).foreach { a: Animal => +// fromJSON[Animal](toJSON(a)) must equal(Valid(a)) // } // } // -// it("must provide derived instances for product types with concrete type parameters") { -// implicit val aToJSON = toJsonProduct(GenericA.apply[String] _) -// implicit val aFromJSON = fromJsonProduct(GenericA.apply[String] _) -// val a = GenericA("hello") -// fromJSON[GenericA[String]](toJSON(a)) must equal(Valid(a)) -// } + it("must provide derived instances for product types with concrete type parameters") { + given ToJSON[GenericA[String]] = ToJSON.derived + given FromJSON[GenericA[String]] = FromJSON.derived + val a = GenericA("hello") + fromJSON[GenericA[String]](toJSON(a)) must equal(Valid(a)) + } // // it("must provide derived instances for singleton objects") { // implicit val toSingletonJSON = toJsonSingleton(Singleton) @@ -382,6 +388,7 @@ case class TestSubjectConcrete1(c1: String) extends TestSubjectCategoryA case class TestSubjectConcrete2(c2: String) extends TestSubjectCategoryA case class TestSubjectConcrete3(c3: String) extends TestSubjectCategoryB +@JSONTypeHint("foo2") case class TestSubjectConcrete4(c4: String) extends TestSubjectCategoryB object TestSubjectCategoryA { @@ -394,11 +401,11 @@ object TestSubjectCategoryB { val json: JSON[TestSubjectCategoryB] = deriveJSON[TestSubjectCategoryB] } -//object TestSubjectBase { -// val json: JSON[TestSubjectBase] = { -// implicit val jsonA = TestSubjectCategoryA.json -// implicit val jsonB = TestSubjectCategoryB.json -// -// jsonTypeSwitch[TestSubjectBase, (TestSubjectCategoryA, TestSubjectCategoryB)]() -// } -//} +object TestSubjectBase { + val json: JSON[TestSubjectBase] = { + given JSON[TestSubjectCategoryA] = TestSubjectCategoryA.json + given JSON[TestSubjectCategoryB] = TestSubjectCategoryB.json + + jsonTypeSwitch[TestSubjectBase, (TestSubjectCategoryA, TestSubjectCategoryB)]() + } +} From 6471cc05577168aa8668fda3f1b9048a0398b981 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 25 Apr 2025 20:27:03 +0200 Subject: [PATCH 095/142] Fix typehints --- .../main/scala-3/io.sphere.json/generic/generic.scala | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala index 15c42b5d..f0559611 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala @@ -82,15 +82,16 @@ inline def jsonTypeSwitch[SuperType, SubTypeTuple <: Tuple](): JSON[SuperType] = val caseClassFormatters = caseClassFormatterList.toMap val allFormattersByTypeName = traitFormatters ++ caseClassFormatters + // We could add some checking here to filter duplicate keys val subTypeHints = - traitFormatters.map((_, formatter) => formatter.traitTypeHintMap).reduce(_ ++ _) - val typeHintMap = traitMetaData.subTypeTypeHints ++ subTypeHints - val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) + traitFormatters.map((_, formatter) => formatter.traitTypeHintMap).fold(Map.empty)(_ ++ _) + val mergedTypeHintMap = traitMetaData.subTypeTypeHints ++ subTypeHints + val reverseTypeHintMap = mergedTypeHintMap.map((on, n) => (n, on)) JSON.instance( writeFn = { a => val originalTypeName = a.asInstanceOf[Product].productPrefix - val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) + val typeName = mergedTypeHintMap.getOrElse(originalTypeName, originalTypeName) val traitFormatterOpt = traitFormatters.get(originalTypeName) traitFormatterOpt .map(_.write(a).asInstanceOf[JObject]) From 67c17a4fbcda85fda8426900e28bdc7cd44e07ef Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Sat, 26 Apr 2025 12:23:51 +0200 Subject: [PATCH 096/142] Better implementation for nested typehint renames in typeswitch --- .../scala-3/io.sphere.json/FromJSON.scala | 11 +------- .../main/scala-3/io.sphere.json/JSON.scala | 2 -- .../generic/AnnotationReader.scala | 2 +- .../generic/DeriveFromJSON.scala | 13 +++------ .../generic/DeriveSingleton.scala | 7 +---- .../io.sphere.json/generic/DeriveToJSON.scala | 11 ++------ .../io.sphere.json/generic/generic.scala | 27 ++++++++++--------- 7 files changed, 22 insertions(+), 51 deletions(-) diff --git a/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala index 80bb08c5..1f0b4a5d 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala @@ -10,27 +10,18 @@ trait FromJSON[A] extends Serializable { /** needed JSON fields - ignored if empty */ val fields: Set[String] = FromJSON.emptyFieldsSet - - /** This is used in the TypeSwitch for cases when there's a nested trait with some type hints on - * the case classes of the nested trait. We somehow need to know about the typehints, with this - * we can propagate them. - */ - val traitTypeHintMap: Map[String, String] = FromJSON.emptyTypeHintMap } object FromJSON extends FromJSONInstances with FromJSONCatsInstances with generic.DeriveFromJSON { val emptyFieldsSet: Set[String] = Set.empty - val emptyTypeHintMap: Map[String, String] = Map.empty inline def apply[A](using instance: FromJSON[A]): FromJSON[A] = instance def instance[A]( readFn: JValue => JValidation[A], - fieldSet: Set[String] = emptyFieldsSet, - typeHintMap: Map[String, String] = emptyTypeHintMap): FromJSON[A] = new { + fieldSet: Set[String] = emptyFieldsSet): FromJSON[A] = new { override def read(jval: JValue): JValidation[A] = readFn(jval) override val fields: Set[String] = fieldSet - override val traitTypeHintMap: Map[String, String] = typeHintMap } } diff --git a/json/json-core/src/main/scala-3/io.sphere.json/JSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/JSON.scala index 56c80c16..94924995 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/JSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/JSON.scala @@ -30,8 +30,6 @@ object JSON extends JSONCatsInstances { override def write(value: A): JValue = toJSON.write(value) override val fields: Set[String] = fromJSON.fields - - override val traitTypeHintMap: Map[String, String] = fromJSON.traitTypeHintMap } } diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/AnnotationReader.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/AnnotationReader.scala index 67540fa1..4d971ed7 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/AnnotationReader.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/AnnotationReader.scala @@ -37,7 +37,7 @@ case class TraitMetaData( val typeDiscriminator: String = typeHintFieldRaw.map(_.value).getOrElse("type") - val subTypeTypeHints: Map[String, String] = subtypes.collect { + val subTypeFieldRenames: Map[String, String] = subtypes.collect { case (name, classMeta) if classMeta.typeHint.isDefined => name -> classMeta.typeHint.get } diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala index 997b72a8..951aeb3c 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala @@ -29,14 +29,8 @@ trait DeriveFromJSON { inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): FromJSON[A] = { val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] - val typeHintMap: Map[String, String] = traitMetaData.subtypes.flatMap { - case (name, classMeta) if classMeta.typeHint.isDefined => - classMeta.typeHint.map(name -> _) - case _ => - None - } - - val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) + val reverseTypeHintMap: Map[String, String] = + traitMetaData.subTypeFieldRenames.map((on, n) => (n, on)) val fromJsons: Seq[FromJSON[Any]] = summonFromJsons[mirrorOfSum.MirroredElemTypes] val names: Seq[String] = @@ -54,8 +48,7 @@ trait DeriveFromJSON { case x => Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$x'")) - }, - typeHintMap = typeHintMap + } ) } diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala index 4d13d279..c51a6557 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala @@ -25,12 +25,7 @@ object DeriveSingleton { inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): JSON[A] = { val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] - val typeHintMap: Map[String, String] = traitMetaData.subtypes.flatMap { - case (name, classMeta) if classMeta.typeHint.isDefined => - classMeta.typeHint.map(name -> _) - case _ => - None - } + val typeHintMap = traitMetaData.subTypeFieldRenames val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) val jsons: Seq[JSON[Any]] = summonFormatters[mirrorOfSum.MirroredElemTypes] diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala index 0e354d79..8bcbec38 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala @@ -30,14 +30,6 @@ trait DeriveToJSON { inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): ToJSON[A] = { val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] - val typeHintMap: Map[String, String] = traitMetaData.subtypes.flatMap { - case (name, classMeta) if classMeta.typeHint.isDefined => - classMeta.typeHint.map(name -> _) - case _ => - None - } - - val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) val jsons: Seq[ToJSON[Any]] = summonToJson[mirrorOfSum.MirroredElemTypes] val names: Seq[String] = @@ -49,7 +41,8 @@ trait DeriveToJSON { ToJSON.instance { value => // we never get a trait here, only classes, it's safe to assume Product val originalTypeName = value.asInstanceOf[Product].productPrefix - val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) + val typeName = + traitMetaData.subTypeFieldRenames.getOrElse(originalTypeName, originalTypeName) val json = jsonsByNames(originalTypeName).write(value).asInstanceOf[JObject] val typeDiscriminator = traitMetaData.typeDiscriminator -> JString(typeName) JObject(typeDiscriminator :: json.obj) diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala index f0559611..5aa177ab 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala @@ -70,33 +70,34 @@ inline def jsonTypeSwitch[SuperType, SubTypeTuple <: Tuple](): JSON[SuperType] = val formattersAndMetaData: Vector[(TraitMetaData, JSON[Any])] = summonFormatters[SubTypeTuple]() // Separate Trait formatters from CaseClass formatters, so we can avoid adding the typeDiscriminator twice - val (traitFormatterList, caseClassFormatterList) = + val (traitInTraitInfo, caseClassFormatters) = formattersAndMetaData.partitionMap { (meta, formatter) => - if (meta.isTrait) - Left(meta.subtypes.map(_._2.name -> formatter)) - else + if (meta.isTrait) { + val formatterByName = meta.subtypes.map((fieldName, m) => m.name -> formatter) + Left((formatterByName, meta.subTypeFieldRenames)) + } else Right(meta.top.name -> formatter) } - val traitFormatters = traitFormatterList.flatten.toMap - val caseClassFormatters = caseClassFormatterList.toMap - val allFormattersByTypeName = traitFormatters ++ caseClassFormatters + // Currently we support 2 layers of traits, because the AnnotationReader only tries to read to 2 levels + val (traitInTraitFormatters, traitInTraitRenames) = traitInTraitInfo.unzip + val traitInTraitFormatterMap = traitInTraitFormatters.fold(Map.empty)(_ ++ _) - // We could add some checking here to filter duplicate keys - val subTypeHints = - traitFormatters.map((_, formatter) => formatter.traitTypeHintMap).fold(Map.empty)(_ ++ _) - val mergedTypeHintMap = traitMetaData.subTypeTypeHints ++ subTypeHints + val caseClassFormatterMap = caseClassFormatters.toMap + val allFormattersByTypeName = traitInTraitFormatterMap ++ caseClassFormatterMap + + val mergedTypeHintMap = traitMetaData.subTypeFieldRenames ++ traitInTraitRenames.fold(Map.empty)(_ ++ _) val reverseTypeHintMap = mergedTypeHintMap.map((on, n) => (n, on)) JSON.instance( writeFn = { a => val originalTypeName = a.asInstanceOf[Product].productPrefix val typeName = mergedTypeHintMap.getOrElse(originalTypeName, originalTypeName) - val traitFormatterOpt = traitFormatters.get(originalTypeName) + val traitFormatterOpt = traitInTraitFormatterMap.get(originalTypeName) traitFormatterOpt .map(_.write(a).asInstanceOf[JObject]) .getOrElse { - val json = caseClassFormatters(originalTypeName).write(a).asInstanceOf[JObject] + val json = caseClassFormatterMap(originalTypeName).write(a).asInstanceOf[JObject] val typeDiscriminator = traitMetaData.typeDiscriminator -> JString(typeName) JObject(typeDiscriminator :: json.obj) } From 4bf30da2fbb2d7f67f292e5c9c77f9745a04a646 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Sat, 26 Apr 2025 21:08:17 +0200 Subject: [PATCH 097/142] add custom implementation test case to typeswitch --- .../io.sphere.json/generic/generic.scala | 25 ++++++++----------- .../json/generic/JsonTypeSwitchSpec.scala | 25 ++++++++++++++++++- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala index 5aa177ab..568b1d92 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala @@ -2,17 +2,7 @@ package io.sphere.json.generic import cats.data.Validated import cats.syntax.validated.* -import io.sphere.json.{ - FromJSON, - JSON, - JSONError, - JSONParseError, - JValidation, - ToJSON, - jsonParseError, - toJSON, - toJValue -} +import io.sphere.json.* import org.json4s.DefaultJsonFormats.given import org.json4s.{JObject, JString, jvalue2monadic, jvalue2readerSyntax} import org.json4s.JsonAST.JValue @@ -86,7 +76,8 @@ inline def jsonTypeSwitch[SuperType, SubTypeTuple <: Tuple](): JSON[SuperType] = val caseClassFormatterMap = caseClassFormatters.toMap val allFormattersByTypeName = traitInTraitFormatterMap ++ caseClassFormatterMap - val mergedTypeHintMap = traitMetaData.subTypeFieldRenames ++ traitInTraitRenames.fold(Map.empty)(_ ++ _) + val mergedTypeHintMap = + traitMetaData.subTypeFieldRenames ++ traitInTraitRenames.fold(Map.empty)(_ ++ _) val reverseTypeHintMap = mergedTypeHintMap.map((on, n) => (n, on)) JSON.instance( @@ -95,11 +86,15 @@ inline def jsonTypeSwitch[SuperType, SubTypeTuple <: Tuple](): JSON[SuperType] = val typeName = mergedTypeHintMap.getOrElse(originalTypeName, originalTypeName) val traitFormatterOpt = traitInTraitFormatterMap.get(originalTypeName) traitFormatterOpt - .map(_.write(a).asInstanceOf[JObject]) + .map(_.write(a)) .getOrElse { - val json = caseClassFormatterMap(originalTypeName).write(a).asInstanceOf[JObject] + val jsonObj = caseClassFormatterMap(originalTypeName).write(a) match { + case JObject(obj) => obj + case json => + throw new Exception(s"This code only handles objects as of now, but got: $json") + } val typeDiscriminator = traitMetaData.typeDiscriminator -> JString(typeName) - JObject(typeDiscriminator :: json.obj) + JObject(typeDiscriminator :: jsonObj) } }, readFn = { diff --git a/json/json-core/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala b/json/json-core/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala index e9bacb0d..7f0fe943 100644 --- a/json/json-core/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala +++ b/json/json-core/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala @@ -1,7 +1,7 @@ package io.sphere.json.generic import cats.data.Validated.Valid -import io.sphere.json.{JSON, deriveJSON} +import io.sphere.json.{JSON, JSONParseError, JValidation, deriveJSON} import io.sphere.json.generic.jsonTypeSwitch import org.json4s.JsonAST.JObject import org.scalatest.matchers.must.Matchers @@ -66,6 +66,29 @@ class JsonTypeSwitchSpec extends AnyWordSpec with Matchers { val messages = jsons.map(Message.json.read).map(_.toOption.get) messages must be(m) } + + "handle custom implementations for subtypes" in { + + given JSON[B] = new JSON[B] { + override def read(jval: JValue): JValidation[B] = jval match { + case JObject(List(_, "field" -> JString(s"Custom-B-${n}"))) => + Valid(B(n.toInt)) + case _ => ??? + } + override def write(value: B): JValue = + JObject(List("field" -> JString(s"Custom-B-${value.int}"))) + } + val format = jsonTypeSwitch[A, (B, D, C)]() + + List( + D(2345), + C(4), + B(34) + ).foreach { value => + val json = format.write(value) + format.read(json).getOrElse(null) must be(value) + } + } } } From fed317f9165a8ef4d839d947fe06562d0c5714fa Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Mon, 28 Apr 2025 18:30:10 +0200 Subject: [PATCH 098/142] Separate JSONTypeSwitch implementation to from and to JSON --- .../generic/AnnotationReader.scala | 7 +- .../generic/EnumerationInstances.scala | 49 ++++++++ .../generic/JSONTypeSwitch.scala | 102 ++++++++++++++++ .../io.sphere.json/generic/generic.scala | 112 ++---------------- .../io/sphere/json/generic/JSONSpec.scala | 66 +++++------ 5 files changed, 200 insertions(+), 136 deletions(-) create mode 100644 json/json-core/src/main/scala-3/io.sphere.json/generic/EnumerationInstances.scala create mode 100644 json/json-core/src/main/scala-3/io.sphere.json/generic/JSONTypeSwitch.scala diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/AnnotationReader.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/AnnotationReader.scala index 4d971ed7..11111799 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/AnnotationReader.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/AnnotationReader.scala @@ -35,7 +35,9 @@ case class TraitMetaData( ) { def isTrait: Boolean = subtypes.nonEmpty - val typeDiscriminator: String = typeHintFieldRaw.map(_.value).getOrElse("type") + private val defaultTypeDiscriminatorName = "type" + val typeDiscriminator: String = + typeHintFieldRaw.map(_.value).getOrElse(defaultTypeDiscriminatorName) val subTypeFieldRenames: Map[String, String] = subtypes.collect { case (name, classMeta) if classMeta.typeHint.isDefined => @@ -122,7 +124,8 @@ class AnnotationReader(using q: Quotes) { private def caseClassMetaData(sym: Symbol): Expr[CaseClassMetaData] = { val caseParams = sym.primaryConstructor.paramSymss.take(1).flatten val fields = Varargs(caseParams.zipWithIndex.map(collectFieldInfo(sym.companionModule))) - val name = Expr(sym.name) + // Removing $ from the end of object names (productPrefix doesn't return names like that, so it's better not to have it) + val name = Expr(sym.name.stripSuffix("$")) val typeHint = sym.annotations.map(findTypeHint).find(_.isDefined).flatten match { case Some(th) => '{ Some($th) } case None => '{ None } diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/EnumerationInstances.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/EnumerationInstances.scala new file mode 100644 index 00000000..adb615db --- /dev/null +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/EnumerationInstances.scala @@ -0,0 +1,49 @@ +package io.sphere.json.generic + +import org.json4s.JsonAST.* +import cats.syntax.validated.* +import io.sphere.json.* + +import scala.collection.mutable + +object EnumerationInstances { + def toJsonEnum(e: Enumeration): ToJSON[e.Value] = new ToJSON[e.Value] { + def write(a: e.Value): JValue = JString(a.toString) + } + + /** Creates a FromJSON instance for an Enumeration type that encodes the `toString` + * representations of the enumeration values. + */ + def fromJsonEnum(e: Enumeration): FromJSON[e.Value] = { + // We're using an AnyRefMap for performance, it's not for mutability. + val validRepresentations = mutable.AnyRefMap( + e.values.iterator.map(value => value.toString -> value.validNel[JSONError]).toSeq: _* + ) + + val allowedValues = e.values.mkString("'", "','", "'") + + new FromJSON[e.Value] { + override def read(json: JValue): JValidation[e.Value] = + json match { + case JString(string) => + validRepresentations.getOrElse( + string, + jsonParseError( + "Invalid enum value: '%s'. Expected one of: %s".format(string, allowedValues))) + case _ => jsonParseError("JSON String expected.") + } + } + } + + // This can be used instead of deriveJSON + def jsonEnum(e: Enumeration): JSON[e.Value] = { + val toJson = toJsonEnum(e) + val fromJson = fromJsonEnum(e) + + new JSON[e.Value] { + override def read(jval: JValue): JValidation[e.Value] = fromJson.read(jval) + + override def write(value: e.Value): JValue = toJson.write(value) + } + } +} diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/JSONTypeSwitch.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/JSONTypeSwitch.scala new file mode 100644 index 00000000..d0264bc3 --- /dev/null +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/JSONTypeSwitch.scala @@ -0,0 +1,102 @@ +package io.sphere.json.generic + +import cats.data.Validated +import io.sphere.json.{FromJSON, JSON, JSONParseError, ToJSON} +import org.json4s.{JObject, JString, jvalue2monadic, jvalue2readerSyntax} +import org.json4s.DefaultJsonFormats.given + +object JSONTypeSwitch { + import scala.compiletime.{erasedValue, error, summonInline} + + case class TraitInformation( + mergedTypeHintMap: Map[String, String], + traitInTraitFormatterMap: Map[String, JSON[Any]], + caseClassFormatterMap: Map[String, JSON[Any]], + traitMetaData: TraitMetaData) + + inline def readTraitInformation[SuperType, SubTypes <: Tuple]: TraitInformation = { + val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] + val formattersAndMetaData: Vector[(TraitMetaData, JSON[Any])] = summonFormatters[SubTypes]() + + // - Separate Trait formatters from CaseClass formatters, so we can avoid adding the typeDiscriminator twice + // - Currently we support 2 layers of traits, because the AnnotationReader only tries to read to 2 levels + val (traitInTraitInfo, caseClassFormatters) = + formattersAndMetaData.partitionMap { (meta, formatter) => + if (meta.isTrait) { + val formatterByName = meta.subtypes.map((fieldName, m) => m.name -> formatter) + Left((formatterByName, meta.subTypeFieldRenames)) + } else { + println(meta.top) + Right(meta.top.name -> formatter) + } + } + + val (traitInTraitFormatters, traitInTraitRenames) = traitInTraitInfo.unzip + val traitInTraitFormatterMap = traitInTraitFormatters.fold(Map.empty)(_ ++ _) + + val caseClassFormatterMap = caseClassFormatters.toMap + + val mergedTypeHintMap = + traitMetaData.subTypeFieldRenames ++ traitInTraitRenames.fold(Map.empty)(_ ++ _) + + TraitInformation( + mergedTypeHintMap, + traitInTraitFormatterMap, + caseClassFormatterMap, + traitMetaData) + } + + inline def toJsonTypeSwitch[SuperType](info: TraitInformation): ToJSON[SuperType] = + ToJSON.instance { a => + val originalTypeName = a.asInstanceOf[Product].productPrefix + val typeName = info.mergedTypeHintMap.getOrElse(originalTypeName, originalTypeName) + val traitFormatterOpt = info.traitInTraitFormatterMap.get(originalTypeName) + traitFormatterOpt + .map(_.write(a)) + .getOrElse { + val jsonObj = info.caseClassFormatterMap(originalTypeName).write(a) match { + case JObject(obj) => obj + case json => + throw new Exception(s"This code only handles objects as of now, but got: $json") + } + val typeDiscriminator = info.traitMetaData.typeDiscriminator -> JString(typeName) + JObject(typeDiscriminator :: jsonObj) + } + } + + inline def fromJsonTypeSwitch[SuperType](info: TraitInformation): FromJSON[SuperType] = { + val reverseTypeHintMap = info.mergedTypeHintMap.map((on, n) => (n, on)) + val allFormattersByTypeName = info.traitInTraitFormatterMap ++ info.caseClassFormatterMap + + FromJSON.instance { + case jObject: JObject => + val typeName = (jObject \ info.traitMetaData.typeDiscriminator).as[String] + val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) + allFormattersByTypeName(originalTypeName).read(jObject).map(_.asInstanceOf[SuperType]) + case x => + Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$x'")) + } + } + + inline def jsonTypeSwitch[SuperType, SubTypes <: Tuple](): JSON[SuperType] = { + val info = readTraitInformation[SuperType, SubTypes] + val fromJson = fromJsonTypeSwitch[SuperType](info) + val toJson = toJsonTypeSwitch[SuperType](info) + + JSON.instance( + writeFn = toJson.write, + readFn = fromJson.read + ) + } + + inline private def summonFormatters[T <: Tuple]( + acc: Vector[(TraitMetaData, JSON[Any])] = Vector.empty): Vector[(TraitMetaData, JSON[Any])] = + inline erasedValue[T] match { + case _: EmptyTuple => acc + case _: (t *: ts) => + val traitMetaData = AnnotationReader.readTraitMetaData[t] + val headFormatter = summonInline[JSON[t]].asInstanceOf[JSON[Any]] + summonFormatters[ts](acc :+ (traitMetaData -> headFormatter)) + } + +} diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala index 568b1d92..31d55e90 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala @@ -1,119 +1,29 @@ package io.sphere.json.generic -import cats.data.Validated -import cats.syntax.validated.* import io.sphere.json.* -import org.json4s.DefaultJsonFormats.given -import org.json4s.{JObject, JString, jvalue2monadic, jvalue2readerSyntax} -import org.json4s.JsonAST.JValue - -import scala.collection.mutable -import scala.compiletime.{erasedValue, error, summonInline} /** Creates a ToJSON instance for an Enumeration type that encodes the `toString` representations of * the enumeration values. */ -def toJsonEnum(e: Enumeration): ToJSON[e.Value] = new ToJSON[e.Value] { - def write(a: e.Value): JValue = JString(a.toString) -} +inline def toJsonEnum(e: Enumeration): ToJSON[e.Value] = EnumerationInstances.toJsonEnum(e) /** Creates a FromJSON instance for an Enumeration type that encodes the `toString` representations * of the enumeration values. */ -def fromJsonEnum(e: Enumeration): FromJSON[e.Value] = { - // We're using an AnyRefMap for performance, it's not for mutability. - val validRepresentations = mutable.AnyRefMap( - e.values.iterator.map { value => - value.toString -> value.validNel[JSONError] - }.toSeq: _* - ) - - val allowedValues = e.values.mkString("'", "','", "'") - - new FromJSON[e.Value] { - override def read(json: JValue): JValidation[e.Value] = - json match { - case JString(string) => - validRepresentations.getOrElse( - string, - jsonParseError( - "Invalid enum value: '%s'. Expected one of: %s".format(string, allowedValues))) - case _ => jsonParseError("JSON String expected.") - } - } -} +inline def fromJsonEnum(e: Enumeration): FromJSON[e.Value] = EnumerationInstances.fromJsonEnum(e) // This can be used instead of deriveJSON -def jsonEnum(e: Enumeration): JSON[e.Value] = { - val toJson = toJsonEnum(e) - val fromJson = fromJsonEnum(e) +inline def jsonEnum(e: Enumeration): JSON[e.Value] = EnumerationInstances.jsonEnum(e) - new JSON[e.Value] { - override def read(jval: JValue): JValidation[e.Value] = fromJson.read(jval) +inline def jsonTypeSwitch[SuperType, SubTypes <: Tuple](): JSON[SuperType] = + JSONTypeSwitch.jsonTypeSwitch[SuperType, SubTypes]() - override def write(value: e.Value): JValue = toJson.write(value) - } +inline def toJsonTypeSwitch[SuperType, SubTypes <: Tuple]: ToJSON[SuperType] = { + val info = JSONTypeSwitch.readTraitInformation[SuperType, SubTypes] + JSONTypeSwitch.toJsonTypeSwitch[SuperType](info) } -inline def jsonTypeSwitch[SuperType, SubTypeTuple <: Tuple](): JSON[SuperType] = { - val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] - val formattersAndMetaData: Vector[(TraitMetaData, JSON[Any])] = summonFormatters[SubTypeTuple]() - - // Separate Trait formatters from CaseClass formatters, so we can avoid adding the typeDiscriminator twice - val (traitInTraitInfo, caseClassFormatters) = - formattersAndMetaData.partitionMap { (meta, formatter) => - if (meta.isTrait) { - val formatterByName = meta.subtypes.map((fieldName, m) => m.name -> formatter) - Left((formatterByName, meta.subTypeFieldRenames)) - } else - Right(meta.top.name -> formatter) - } - - // Currently we support 2 layers of traits, because the AnnotationReader only tries to read to 2 levels - val (traitInTraitFormatters, traitInTraitRenames) = traitInTraitInfo.unzip - val traitInTraitFormatterMap = traitInTraitFormatters.fold(Map.empty)(_ ++ _) - - val caseClassFormatterMap = caseClassFormatters.toMap - val allFormattersByTypeName = traitInTraitFormatterMap ++ caseClassFormatterMap - - val mergedTypeHintMap = - traitMetaData.subTypeFieldRenames ++ traitInTraitRenames.fold(Map.empty)(_ ++ _) - val reverseTypeHintMap = mergedTypeHintMap.map((on, n) => (n, on)) - - JSON.instance( - writeFn = { a => - val originalTypeName = a.asInstanceOf[Product].productPrefix - val typeName = mergedTypeHintMap.getOrElse(originalTypeName, originalTypeName) - val traitFormatterOpt = traitInTraitFormatterMap.get(originalTypeName) - traitFormatterOpt - .map(_.write(a)) - .getOrElse { - val jsonObj = caseClassFormatterMap(originalTypeName).write(a) match { - case JObject(obj) => obj - case json => - throw new Exception(s"This code only handles objects as of now, but got: $json") - } - val typeDiscriminator = traitMetaData.typeDiscriminator -> JString(typeName) - JObject(typeDiscriminator :: jsonObj) - } - }, - readFn = { - case jObject: JObject => - val typeName = (jObject \ traitMetaData.typeDiscriminator).as[String] - val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - allFormattersByTypeName(originalTypeName).read(jObject).map(_.asInstanceOf[SuperType]) - case x => - Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$x'")) - } - ) +inline def fromJsonTypeSwitch[SuperType, SubTypes <: Tuple]: FromJSON[SuperType] = { + val info = JSONTypeSwitch.readTraitInformation[SuperType, SubTypes] + JSONTypeSwitch.fromJsonTypeSwitch[SuperType](info) } - -inline private def summonFormatters[T <: Tuple]( - acc: Vector[(TraitMetaData, JSON[Any])] = Vector.empty): Vector[(TraitMetaData, JSON[Any])] = - inline erasedValue[T] match { - case _: EmptyTuple => acc - case _: (t *: ts) => - val traitMetaData = AnnotationReader.readTraitMetaData[t] - val headFormatter = summonInline[JSON[t]].asInstanceOf[JSON[Any]] - summonFormatters[ts](acc :+ (traitMetaData -> headFormatter)) - } diff --git a/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala b/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala index bbe6cf38..faf62d25 100644 --- a/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala +++ b/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala @@ -263,23 +263,23 @@ class JSONSpec extends AnyFunSpec with Matchers { } } -// it("must provide derived JSON instances for sum types") { -// // ToJSON -// implicit val birdToJSON = deriveJSON[Bird].write(Bird.apply _) -// implicit val dogToJSON = deriveJSON[Dog].write(Dog.apply _) -// implicit val catToJSON = ToJSON.derived[Cat] -// implicit val animalToJSON = jsonTypeSwitch[Animal, (Bird, Dog, Cat)]() -// // FromJSON -// implicit val birdFromJSON = FromJSON.derived[Bird] -// implicit val dogFromJSON = FromJSON.derived[Dog] -// implicit val catFromJSON = FromJSON.derived[Cat] -// implicit val animalFromJSON = jsonTypeSwitch[Animal, (Bird, Dog, Cat)]() -// -// List(Bird("Peewee"), Dog("Hasso"), Cat("Felidae")).foreach { a: Animal => -// fromJSON[Animal](toJSON(a)) must equal(Valid(a)) -// } -// } -// + it("must provide derived JSON instances for sum types") { + // ToJSON + given ToJSON[Bird] = ToJSON.derived[Bird] + given ToJSON[Dog] = ToJSON.derived[Dog] + given ToJSON[Cat] = ToJSON.derived[Cat] + given ToJSON[Animal] = toJsonTypeSwitch[Animal, (Bird, Dog, Cat)] + // FromJSON + given FromJSON[Bird] = FromJSON.derived[Bird] + given FromJSON[Dog] = FromJSON.derived[Dog] + given FromJSON[Cat] = FromJSON.derived[Cat] + given FromJSON[Animal] = fromJsonTypeSwitch[Animal, (Bird, Dog, Cat)] + + List(Bird("Peewee"), Dog("Hasso"), Cat("Felidae")).foreach { (a: Animal) => + fromJSON[Animal](toJSON(a)) must equal(Valid(a)) + } + } + it("must provide derived instances for product types with concrete type parameters") { given ToJSON[GenericA[String]] = ToJSON.derived given FromJSON[GenericA[String]] = FromJSON.derived @@ -317,22 +317,22 @@ class JSONSpec extends AnyFunSpec with Matchers { // fromJSON[SingletonEnum](toJSON(s)) must equal(Valid(s)) // } // } -// -// it("must provide derived instances for sum types with a mix of case class / object") { -// // ToJSON -// implicit val toSingleJSON = toJsonProduct0(SingletonMixed) -// implicit val toRecordJSON = toJsonProduct(RecordMixed.apply _) -// implicit val toMixedJSON = toJsonTypeSwitch[Mixed, SingletonMixed.type, RecordMixed](Nil) -// // FromJSON -// implicit val fromSingleJSON = fromJsonProduct0(SingletonMixed) -// implicit val fromRecordJSON = fromJsonProduct(RecordMixed.apply _) -// implicit val fromMixedJSON = fromJsonTypeSwitch[Mixed, SingletonMixed.type, RecordMixed](Nil) -// List(SingletonMixed, RecordMixed(1)).foreach { -// m: Mixed => -// fromJSON[Mixed](toJSON(m)) must equal(Valid(m)) -// } -// } -// + + it("must provide derived instances for sum types with a mix of case class / object") { + // ToJSON + given ToJSON[SingletonMixed.type] = ToJSON.derived + given ToJSON[RecordMixed] = ToJSON.derived + given ToJSON[Mixed] = toJsonTypeSwitch[Mixed, (SingletonMixed.type, RecordMixed)] + // FromJSON + given FromJSON[SingletonMixed.type] = FromJSON.derived + given FromJSON[RecordMixed] = FromJSON.derived + given FromJSON[Mixed] = fromJsonTypeSwitch[Mixed, (SingletonMixed.type, RecordMixed)] + + List(SingletonMixed, RecordMixed(1)).foreach { m => + fromJSON[Mixed](toJSON(m)) must equal(Valid(m)) + } + } + // // it("must handle subclasses correctly in `jsonTypeSwitch`") { // // ToJSON From d0e2bd11e6ee9a8be3f2b1a62dc3bc735e6c09db Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Mon, 28 Apr 2025 18:36:34 +0200 Subject: [PATCH 099/142] Adding last test in JSONSpec --- .../io/sphere/json/generic/JSONSpec.scala | 112 +++++++----------- 1 file changed, 40 insertions(+), 72 deletions(-) diff --git a/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala b/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala index faf62d25..211016b3 100644 --- a/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala +++ b/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala @@ -286,37 +286,6 @@ class JSONSpec extends AnyFunSpec with Matchers { val a = GenericA("hello") fromJSON[GenericA[String]](toJSON(a)) must equal(Valid(a)) } -// -// it("must provide derived instances for singleton objects") { -// implicit val toSingletonJSON = toJsonSingleton(Singleton) -// implicit val fromSingletonJSON = fromJsonSingleton(Singleton) -// val json = s"""[${toJSON(Singleton)}]""" -// withClue(json) { -// fromJSON[Seq[Singleton.type]](json) must equal(Valid(Seq(Singleton))) -// } -// -// // ToJSON -// implicit val toSingleAJSON = toJsonSingleton(SingletonA) -// implicit val toSingleBJSON = toJsonSingleton(SingletonB) -// implicit val toSingleCJSON = toJsonSingleton(SingletonC) -// implicit val toSingleEnumJSON = -// toJsonSingletonEnumSwitch[SingletonEnum, SingletonA.type, SingletonB.type, SingletonC.type]( -// Nil) -// // FromJSON -// implicit val fromSingleAJSON = fromJsonSingleton(SingletonA) -// implicit val fromSingleBJSON = fromJsonSingleton(SingletonB) -// implicit val fromSingleCJSON = fromJsonSingleton(SingletonC) -// implicit val fromSingleEnumJSON = fromJsonSingletonEnumSwitch[ -// SingletonEnum, -// SingletonA.type, -// SingletonB.type, -// SingletonC.type](Nil) -// -// List(SingletonA, SingletonB, SingletonC).foreach { -// s: SingletonEnum => -// fromJSON[SingletonEnum](toJSON(s)) must equal(Valid(s)) -// } -// } it("must provide derived instances for sum types with a mix of case class / object") { // ToJSON @@ -333,47 +302,46 @@ class JSONSpec extends AnyFunSpec with Matchers { } } -// -// it("must handle subclasses correctly in `jsonTypeSwitch`") { -// // ToJSON -// implicit val to1 = toJsonProduct(TestSubjectConcrete1.apply _) -// implicit val to2 = toJsonProduct(TestSubjectConcrete2.apply _) -// implicit val to3 = toJsonProduct(TestSubjectConcrete3.apply _) -// implicit val to4 = toJsonProduct(TestSubjectConcrete4.apply _) -// implicit val toA = -// toJsonTypeSwitch[TestSubjectCategoryA, TestSubjectConcrete1, TestSubjectConcrete2](Nil) -// implicit val toB = -// toJsonTypeSwitch[TestSubjectCategoryB, TestSubjectConcrete3, TestSubjectConcrete4](Nil) -// implicit val toBase = -// toJsonTypeSwitch[TestSubjectBase, TestSubjectCategoryA, TestSubjectCategoryB](Nil) -// -// // FromJSON -// implicit val from1 = fromJsonProduct(TestSubjectConcrete1.apply _) -// implicit val from2 = fromJsonProduct(TestSubjectConcrete2.apply _) -// implicit val from3 = fromJsonProduct(TestSubjectConcrete3.apply _) -// implicit val from4 = fromJsonProduct(TestSubjectConcrete4.apply _) -// implicit val fromA = -// fromJsonTypeSwitch[TestSubjectCategoryA, TestSubjectConcrete1, TestSubjectConcrete2](Nil) -// implicit val fromB = -// fromJsonTypeSwitch[TestSubjectCategoryB, TestSubjectConcrete3, TestSubjectConcrete4](Nil) -// implicit val fromBase = -// fromJsonTypeSwitch[TestSubjectBase, TestSubjectCategoryA, TestSubjectCategoryB](Nil) -// -// val testSubjects = List[TestSubjectBase]( -// TestSubjectConcrete1("testSubject1"), -// TestSubjectConcrete2("testSubject2"), -// TestSubjectConcrete3("testSubject3"), -// TestSubjectConcrete4("testSubject4") -// ) -// -// testSubjects.foreach { testSubject => -// val json = toJSON(testSubject) -// withClue(json) { -// fromJSON[TestSubjectBase](json) must equal(Valid(testSubject)) -// } -// } -// -// } + it("must handle subclasses correctly in `jsonTypeSwitch`") { + // ToJSON + given ToJSON[TestSubjectConcrete1] = ToJSON.derived + given ToJSON[TestSubjectConcrete2] = ToJSON.derived + given ToJSON[TestSubjectConcrete3] = ToJSON.derived + given ToJSON[TestSubjectConcrete4] = ToJSON.derived + given ToJSON[TestSubjectCategoryA] = + toJsonTypeSwitch[TestSubjectCategoryA, (TestSubjectConcrete1, TestSubjectConcrete2)] + given ToJSON[TestSubjectCategoryB] = + toJsonTypeSwitch[TestSubjectCategoryB, (TestSubjectConcrete3, TestSubjectConcrete4)] + given ToJSON[TestSubjectBase] = + toJsonTypeSwitch[TestSubjectBase, (TestSubjectCategoryA, TestSubjectCategoryB)] + + // FromJSON + given FromJSON[TestSubjectConcrete1] = FromJSON.derived + given FromJSON[TestSubjectConcrete2] = FromJSON.derived + given FromJSON[TestSubjectConcrete3] = FromJSON.derived + given FromJSON[TestSubjectConcrete4] = FromJSON.derived + given FromJSON[TestSubjectCategoryA] = + fromJsonTypeSwitch[TestSubjectCategoryA, (TestSubjectConcrete1, TestSubjectConcrete2)] + given FromJSON[TestSubjectCategoryB] = + fromJsonTypeSwitch[TestSubjectCategoryB, (TestSubjectConcrete3, TestSubjectConcrete4)] + given FromJSON[TestSubjectBase] = + fromJsonTypeSwitch[TestSubjectBase, (TestSubjectCategoryA, TestSubjectCategoryB)] + + val testSubjects = List[TestSubjectBase]( + TestSubjectConcrete1("testSubject1"), + TestSubjectConcrete2("testSubject2"), + TestSubjectConcrete3("testSubject3"), + TestSubjectConcrete4("testSubject4") + ) + + testSubjects.foreach { testSubject => + val json = toJSON(testSubject) + withClue(json) { + fromJSON[TestSubjectBase](json) must equal(Valid(testSubject)) + } + } + + } } } From 24ad8169bdeb9655ab9a2a61a5d3fc9b81ed1e9d Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 1 May 2025 15:39:32 +0200 Subject: [PATCH 100/142] Fix bug regards to nested type class derivation, Rename CaseClassMetaData -> TypeMetaData, review comment changes --- .../generic/AnnotationReader.scala | 28 +-- .../generic/DeriveFromJSON.scala | 4 +- .../io.sphere.json/generic/DeriveToJSON.scala | 2 +- .../generic/JSONTypeSwitch.scala | 1 - .../io/sphere/mongo/format/MongoFormat.scala | 168 +++++++++++------- .../mongo/generic/AnnotationReader.scala | 28 +-- .../io/sphere/mongo/generic/generic.scala | 8 +- .../io/sphere/mongo/DerivationSpec.scala | 2 +- .../mongo/generic/SumTypesDerivingSpec.scala | 18 +- util/src/main/scala-3/VectorUtils.scala | 19 ++ 10 files changed, 165 insertions(+), 113 deletions(-) create mode 100644 util/src/main/scala-3/VectorUtils.scala diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/AnnotationReader.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/AnnotationReader.scala index 11111799..02771a7c 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/AnnotationReader.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/AnnotationReader.scala @@ -16,7 +16,7 @@ case class Field( val fieldName: String = jsonKey.map(_.value).getOrElse(name) } -case class CaseClassMetaData( +case class TypeMetaData( name: String, typeHintRaw: Option[JSONTypeHint], fields: Vector[Field] @@ -29,9 +29,9 @@ case class CaseClassMetaData( * field would be populated */ case class TraitMetaData( - top: CaseClassMetaData, + top: TypeMetaData, typeHintFieldRaw: Option[JSONTypeHintField], - subtypes: Map[String, CaseClassMetaData] + subtypes: Map[String, TypeMetaData] ) { def isTrait: Boolean = subtypes.nonEmpty @@ -49,9 +49,9 @@ class AnnotationReader(using q: Quotes) { import q.reflect.* - def readCaseClassMetaData[T: Type]: Expr[CaseClassMetaData] = { + def readTypeMetaData[T: Type]: Expr[TypeMetaData] = { val sym = TypeRepr.of[T].typeSymbol - caseClassMetaData(sym) + typeMetaData(sym) } def readTraitMetaData[T: Type]: Expr[TraitMetaData] = { @@ -64,7 +64,7 @@ class AnnotationReader(using q: Quotes) { '{ TraitMetaData( - top = ${ caseClassMetaData(sym) }, + top = ${ typeMetaData(sym) }, typeHintFieldRaw = $typeHintField, subtypes = ${ subtypeAnnotations(sym) } ) @@ -121,7 +121,7 @@ class AnnotationReader(using q: Quotes) { } } - private def caseClassMetaData(sym: Symbol): Expr[CaseClassMetaData] = { + private def typeMetaData(sym: Symbol): Expr[TypeMetaData] = { val caseParams = sym.primaryConstructor.paramSymss.take(1).flatten val fields = Varargs(caseParams.zipWithIndex.map(collectFieldInfo(sym.companionModule))) // Removing $ from the end of object names (productPrefix doesn't return names like that, so it's better not to have it) @@ -132,7 +132,7 @@ class AnnotationReader(using q: Quotes) { } '{ - CaseClassMetaData( + TypeMetaData( name = $name, typeHintRaw = $typeHint, fields = Vector($fields*) @@ -140,13 +140,13 @@ class AnnotationReader(using q: Quotes) { } } - private def subtypeAnnotation(sym: Symbol): Expr[(String, CaseClassMetaData)] = { + private def subtypeAnnotation(sym: Symbol): Expr[(String, TypeMetaData)] = { val name = Expr(sym.name) - val annots = caseClassMetaData(sym) + val annots = typeMetaData(sym) '{ ($name, $annots) } } - private def subtypeAnnotations(sym: Symbol): Expr[Map[String, CaseClassMetaData]] = { + private def subtypeAnnotations(sym: Symbol): Expr[Map[String, TypeMetaData]] = { val subtypes = Varargs(sym.children.map(subtypeAnnotation)) '{ Map($subtypes*) } } @@ -154,12 +154,12 @@ class AnnotationReader(using q: Quotes) { } object AnnotationReader { - inline def readCaseClassMetaData[T]: CaseClassMetaData = ${ readCaseClassMetaDataImpl[T] } + inline def readTypeMetaData[T]: TypeMetaData = ${ readTypeMetaDataImpl[T] } inline def readTraitMetaData[T]: TraitMetaData = ${ readTraitMetaDataImpl[T] } - private def readCaseClassMetaDataImpl[T: Type](using Quotes): Expr[CaseClassMetaData] = - AnnotationReader().readCaseClassMetaData[T] + private def readTypeMetaDataImpl[T: Type](using Quotes): Expr[TypeMetaData] = + AnnotationReader().readTypeMetaData[T] private def readTraitMetaDataImpl[T: Type](using Quotes): Expr[TraitMetaData] = AnnotationReader().readTraitMetaData[T] diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala index 951aeb3c..ed05c3aa 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala @@ -3,7 +3,7 @@ package io.sphere.json.generic import cats.data.Validated import cats.syntax.traverse.* import io.sphere.json.field -import io.sphere.json.generic.{AnnotationReader, CaseClassMetaData, Field, TraitMetaData} +import io.sphere.json.generic.{AnnotationReader, TypeMetaData, Field, TraitMetaData} import org.json4s.JsonAST.* import org.json4s.DefaultReaders.StringReader import org.json4s.{jvalue2monadic, jvalue2readerSyntax} @@ -53,7 +53,7 @@ trait DeriveFromJSON { } inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): FromJSON[A] = { - val caseClassMetaData: CaseClassMetaData = AnnotationReader.readCaseClassMetaData[A] + val caseClassMetaData: TypeMetaData = AnnotationReader.readTypeMetaData[A] val fromJsons: Vector[FromJSON[Any]] = summonFromJsons[mirrorOfProduct.MirroredElemTypes] val fieldsAndJsons: Vector[(Field, FromJSON[Any])] = diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala index 8bcbec38..194960d9 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala @@ -50,7 +50,7 @@ trait DeriveToJSON { } inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): ToJSON[A] = { - val caseClassMetaData: CaseClassMetaData = AnnotationReader.readCaseClassMetaData[A] + val caseClassMetaData: TypeMetaData = AnnotationReader.readTypeMetaData[A] val toJsons: Vector[ToJSON[Any]] = summonToJson[mirrorOfProduct.MirroredElemTypes] ToJSON.instance { value => diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/JSONTypeSwitch.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/JSONTypeSwitch.scala index d0264bc3..e89306b5 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/JSONTypeSwitch.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/JSONTypeSwitch.scala @@ -26,7 +26,6 @@ object JSONTypeSwitch { val formatterByName = meta.subtypes.map((fieldName, m) => m.name -> formatter) Left((formatterByName, meta.subTypeFieldRenames)) } else { - println(meta.top) Right(meta.top.name -> formatter) } } diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala index 80bc9d7c..bbcf193d 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala @@ -2,6 +2,7 @@ package io.sphere.mongo.format import com.mongodb.BasicDBObject import io.sphere.mongo.generic.{AnnotationReader, Field} +import io.sphere.util.VectorUtils.* import org.bson.types.ObjectId import java.util.UUID @@ -21,6 +22,27 @@ trait MongoFormat[A] extends Serializable { def default: Option[A] = None } +/** Some extra information for traits and abstract classes so we can handle nested hierarchies + * easier + */ +trait TraitMongoFormat[A] extends MongoFormat[A] { + val subTypeNames: Vector[String] + val typeDiscriminator: String +} + +object TraitMongoFormat { + def instance[A]( + fromMongo: Any => A, + toMongo: A => Any, + subTypes: Vector[String], + typeDiscr: String): TraitMongoFormat[A] = new { + override def toMongoValue(a: A): Any = toMongo(a) + override def fromMongoValue(mongoType: Any): A = fromMongo(mongoType) + override val subTypeNames: Vector[String] = subTypes + override val typeDiscriminator: String = typeDiscr + } +} + inline def deriveMongoFormat[A](using Mirror.Of[A]): MongoFormat[A] = MongoFormat.derived object MongoFormat { @@ -30,12 +52,12 @@ object MongoFormat { inline given derived[A](using Mirror.Of[A]): MongoFormat[A] = Derivation.derived def instance[A]( - fromFn: Any => A, - toFn: A => Any, + fromMongo: Any => A, + toMongo: A => Any, fieldSet: Set[String] = emptyFields): MongoFormat[A] = new { - override def toMongoValue(a: A): Any = toFn(a) - override def fromMongoValue(mongoType: Any): A = fromFn(mongoType) + override def toMongoValue(a: A): Any = toMongo(a) + override def fromMongoValue(mongoType: Any): A = fromMongo(mongoType) override val fields: Set[String] = fieldSet } @@ -50,7 +72,7 @@ object MongoFormat { } private object Derivation { - import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} + import scala.compiletime.{constValue, constValueTuple, erasedValue, error, summonInline} inline def derived[A](using m: Mirror.Of[A]): MongoFormat[A] = inline m match { @@ -58,18 +80,26 @@ object MongoFormat { case p: Mirror.ProductOf[A] => deriveCaseClass(p) } - @annotation.nowarn("msg=New anonymous class definition will be duplicated at each inline site") - inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): MongoFormat[A] = - new MongoFormat[A] { - private val traitMetaData = AnnotationReader.readTraitMetaData[A] - private val typeHintMap = traitMetaData.subTypeTypeHints - private val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) - private val formatters = summonFormatters[mirrorOfSum.MirroredElemTypes] - private val names = constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector - .asInstanceOf[Vector[String]] - private val formattersByTypeName = names.zip(formatters).toMap - - override def toMongoValue(a: A): Any = { + inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): MongoFormat[A] = { + val traitMetaData = AnnotationReader.readTraitMetaData[A] + val typeHintMap = traitMetaData.subTypeTypeHints + val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) + val formatters = summonFormatters[mirrorOfSum.MirroredElemTypes] + val names = constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector + .asInstanceOf[Vector[String]] + val formattersByTypeName = names + .zip(formatters) + .flatMap { case kv @ (name, formatter) => + formatter match { + case traitFormatter: TraitMongoFormat[_] => + traitFormatter.subTypeNames.map(_ -> formatter) + case _ => Vector(kv) + } + } + .toMapWithNoDuplicateKeys + + TraitMongoFormat.instance( + toMongo = { a => // we never get a trait here, only classes, it's safe to assume Product val originalTypeName = a.asInstanceOf[Product].productPrefix val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) @@ -77,31 +107,31 @@ object MongoFormat { formattersByTypeName(originalTypeName).toMongoValue(a).asInstanceOf[BasicDBObject] bson.put(traitMetaData.typeDiscriminator, typeName) bson - } - - override def fromMongoValue(bson: Any): A = - bson match { - case bson: BasicDBObject => - val typeName = bson.get(traitMetaData.typeDiscriminator).asInstanceOf[String] - val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - formattersByTypeName(originalTypeName).fromMongoValue(bson).asInstanceOf[A] - case x => - throw new Exception(s"BsonObject is expected for a Trait subtype, instead got $x") - } - } - - @annotation.nowarn("msg=New anonymous class definition will be duplicated at each inline site") - inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): MongoFormat[A] = - new MongoFormat[A] { - private val caseClassMetaData = AnnotationReader.readCaseClassMetaData[A] - private val formatters = summonFormatters[mirrorOfProduct.MirroredElemTypes] - private val fieldsAndFormatters = caseClassMetaData.fields.zip(formatters) - - override val fields: Set[String] = fieldsAndFormatters.toSet.flatMap((field, formatter) => - if (field.embedded) formatter.fields + field.rawName - else Set(field.rawName)) - - override def toMongoValue(a: A): Any = { + }, + fromMongo = { + case bson: BasicDBObject => + val typeName = bson.get(traitMetaData.typeDiscriminator).asInstanceOf[String] + val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) + formattersByTypeName(originalTypeName).fromMongoValue(bson).asInstanceOf[A] + case x => + throw new Exception(s"BsonObject is expected for a Trait subtype, instead got $x") + }, + subTypes = names, + typeDiscr = traitMetaData.typeDiscriminator + ) + } + + inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): MongoFormat[A] = { + val caseClassMetaData = AnnotationReader.readTypeMetaData[A] + val formatters = summonFormatters[mirrorOfProduct.MirroredElemTypes] + val fieldsAndFormatters = caseClassMetaData.fields.zip(formatters) + + val fields: Set[String] = fieldsAndFormatters.toSet.flatMap((field, formatter) => + if (field.embedded) formatter.fields + field.rawName + else Set(field.rawName)) + + instance( + toMongo = { a => val bson = new BasicDBObject() val values = a.asInstanceOf[Product].productIterator formatters.zip(values).zip(caseClassMetaData.fields).foreach { @@ -109,37 +139,37 @@ object MongoFormat { addField(bson, field, format.toMongoValue(value)) } bson - } - - override def fromMongoValue(mongoType: Any): A = - mongoType match { - case bson: BasicDBObject => - val fields = fieldsAndFormatters - .map { case (field, format) => - def defaultValue = field.defaultArgument.orElse(format.default) - - if (field.ignored) + }, + fromMongo = { + case bson: BasicDBObject => + val fields = fieldsAndFormatters + .map { case (field, format) => + def defaultValue = field.defaultArgument.orElse(format.default) + + if (field.ignored) + defaultValue.getOrElse { + throw new Exception( + s"Missing default parameter value for ignored field `${field.name}` on deserialization.") + } + else if (field.embedded) format.fromMongoValue(bson) + else { + val value = bson.get(field.name) + if (value ne null) format.fromMongoValue(value.asInstanceOf[Any]) + else defaultValue.getOrElse { throw new Exception( - s"Missing default parameter value for ignored field `${field.name}` on deserialization.") + s"Missing required field '${field.name}' on deserialization.") } - else if (field.embedded) format.fromMongoValue(bson) - else { - val value = bson.get(field.name) - if (value ne null) format.fromMongoValue(value.asInstanceOf[Any]) - else - defaultValue.getOrElse { - throw new Exception( - s"Missing required field '${field.name}' on deserialization.") - } - } } - val tuple = Tuple.fromArray(fields.toArray) - mirrorOfProduct.fromTuple(tuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) - - case x => throw new Exception(s"BasicDBObject is expected for a class, instead got: $x") - } - } + } + val tuple = Tuple.fromArray(fields.toArray) + mirrorOfProduct.fromTuple(tuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) + + case x => throw new Exception(s"BasicDBObject is expected for a class, instead got: $x") + }, + fieldSet = fields + ) + } inline private def summonFormatters[T <: Tuple]: Vector[MongoFormat[Any]] = inline erasedValue[T] match { diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/AnnotationReader.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/AnnotationReader.scala index 2a5a4a99..94c48e87 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/AnnotationReader.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/AnnotationReader.scala @@ -13,7 +13,7 @@ case class Field( defaultArgument: Option[Any]) { val name: String = mongoKey.map(_.value).getOrElse(rawName) } -case class CaseClassMetaData( +case class TypeMetaData( name: String, typeHintRaw: Option[MongoTypeHint], fields: Vector[Field] @@ -23,9 +23,9 @@ case class CaseClassMetaData( } case class TraitMetaData( - top: CaseClassMetaData, + top: TypeMetaData, typeHintFieldRaw: Option[MongoTypeHintField], - subtypes: Map[String, CaseClassMetaData] + subtypes: Map[String, TypeMetaData] ) { val typeDiscriminator: String = typeHintFieldRaw.map(_.value).getOrElse("type") @@ -39,10 +39,10 @@ object AnnotationReader { inline def readTraitMetaData[T]: TraitMetaData = ${ readTraitMetaDataImpl[T] } - inline def readCaseClassMetaData[T]: CaseClassMetaData = ${ readCaseClassMetaDataImpl[T] } + inline def readTypeMetaData[T]: TypeMetaData = ${ readTypeMetaDataImpl[T] } - private def readCaseClassMetaDataImpl[T: Type](using Quotes): Expr[CaseClassMetaData] = - AnnotationReader().readCaseClassMetaData[T] + private def readTypeMetaDataImpl[T: Type](using Quotes): Expr[TypeMetaData] = + AnnotationReader().readTypeMetaData[T] private def readTraitMetaDataImpl[T: Type](using Quotes): Expr[TraitMetaData] = AnnotationReader().readTraitMetaData[T] @@ -51,9 +51,9 @@ object AnnotationReader { class AnnotationReader(using q: Quotes) { import q.reflect.* - def readCaseClassMetaData[T: Type]: Expr[CaseClassMetaData] = { + def readTypeMetaData[T: Type]: Expr[TypeMetaData] = { val sym = TypeRepr.of[T].typeSymbol - caseClassMetaData(sym) + typeMetaData(sym) } def readTraitMetaData[T: Type]: Expr[TraitMetaData] = { @@ -66,7 +66,7 @@ class AnnotationReader(using q: Quotes) { '{ TraitMetaData( - top = ${ caseClassMetaData(sym) }, + top = ${ typeMetaData(sym) }, typeHintFieldRaw = $typeHintField, subtypes = ${ subtypeAnnotations(sym) } ) @@ -123,7 +123,7 @@ class AnnotationReader(using q: Quotes) { } } - private def caseClassMetaData(sym: Symbol): Expr[CaseClassMetaData] = { + private def typeMetaData(sym: Symbol): Expr[TypeMetaData] = { val caseParams = sym.primaryConstructor.paramSymss.take(1).flatten val fields = Varargs(caseParams.zipWithIndex.map(collectFieldInfo(sym.companionModule))) val name = Expr(sym.name) @@ -133,7 +133,7 @@ class AnnotationReader(using q: Quotes) { } '{ - CaseClassMetaData( + TypeMetaData( name = $name, typeHintRaw = $typeHint, fields = Vector($fields*) @@ -141,13 +141,13 @@ class AnnotationReader(using q: Quotes) { } } - private def subtypeAnnotation(sym: Symbol): Expr[(String, CaseClassMetaData)] = { + private def subtypeAnnotation(sym: Symbol): Expr[(String, TypeMetaData)] = { val name = Expr(sym.name) - val annots = caseClassMetaData(sym) + val annots = typeMetaData(sym) '{ ($name, $annots) } } - private def subtypeAnnotations(sym: Symbol): Expr[Map[String, CaseClassMetaData]] = { + private def subtypeAnnotations(sym: Symbol): Expr[Map[String, TypeMetaData]] = { val subtypes = Varargs(sym.children.map(subtypeAnnotation)) '{ Map($subtypes*) } } diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala index 24d38941..4a0590af 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala @@ -21,7 +21,7 @@ inline def mongoTypeSwitch[SuperType, SubTypeTuple <: Tuple](): MongoFormat[Supe val formattersByTypeName = names.zip(formatters).toMap MongoFormat.instance( - toFn = { a => + toMongo = { a => val originalTypeName = a.asInstanceOf[Product].productPrefix val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) val bson = @@ -29,7 +29,7 @@ inline def mongoTypeSwitch[SuperType, SubTypeTuple <: Tuple](): MongoFormat[Supe bson.put(traitMetaData.typeDiscriminator, typeName) bson }, - fromFn = { + fromMongo = { case bson: BasicDBObject => val typeName = bson.get(traitMetaData.typeDiscriminator).asInstanceOf[String] val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) @@ -44,11 +44,11 @@ private def findTypeValue(dbo: BSONObject, typeField: String): Option[String] = Option(dbo.get(typeField)).map(_.toString) inline private def summonMetaData[T <: Tuple]( - acc: Vector[CaseClassMetaData] = Vector.empty): Vector[CaseClassMetaData] = + acc: Vector[TypeMetaData] = Vector.empty): Vector[TypeMetaData] = inline erasedValue[T] match { case _: EmptyTuple => acc case _: (t *: ts) => - summonMetaData[ts](acc :+ AnnotationReader.readCaseClassMetaData[t]) + summonMetaData[ts](acc :+ AnnotationReader.readTypeMetaData[t]) } inline private def summonFormatters[T <: Tuple]( diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/DerivationSpec.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/DerivationSpec.scala index 469d79fc..29e69458 100644 --- a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/DerivationSpec.scala +++ b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/DerivationSpec.scala @@ -50,7 +50,7 @@ class DerivationSpec extends AnyWordSpec with Matchers { case object Object2 extends Root case class Class(i: Int, @MongoEmbedded inner: InnerClass) extends Root - val res = AnnotationReader.readCaseClassMetaData[Root] + val res = AnnotationReader.readTypeMetaData[Root] } } diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala index dfe93126..d7b83365 100644 --- a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala +++ b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala @@ -37,15 +37,17 @@ class SumTypesDerivingSpec extends AnyWordSpec with Matchers { check(Color4.format, Color4.Custom("2356"), dbObj("color" -> "custom", "rgb" -> "2356")) } - "not allow specifying different custom field" in pendingUntilFixed { - // to serialize Custom, should we use type "color" or "color-custom"? - // The current implementation just takes the type hint of the trait and doesn't look at the subtypes anyway - "deriveMongoFormat[Color5]" mustNot compile + "ignore @MongoTypeHintField on subtypes if it's present on the trait" in { + check(Color5.format, Color5.Red, dbObj("color" -> "red")) + + check(Color5.format, Color5.Custom("123"), dbObj("color" -> "custom", "rgb" -> "123")) } "not allow specifying different custom field on intermediate level" in { - // to serialize Custom, should we use type "color" or "color-custom"? - "deriveMongoFormat[Color6]" mustNot compile + // TODO make this more sophisticated in the future + // The color is Red because the top level trait doesn't read the annotations of children of it's children + check(Color6.format, Color6.Red, dbObj("color-custom" -> "red", "color" -> "Red")) + } "use intermediate level" in { @@ -160,17 +162,19 @@ object SumTypesDerivingSpec { @MongoTypeHintField("color-custom") @MongoTypeHint("custom") case class Custom(rgb: String) extends Color5 + val format = deriveMongoFormat[Color5] } @MongoTypeHintField("color") sealed trait Color6 object Color6 { @MongoTypeHintField("color-custom") - abstract class MyColor extends Color6 + sealed abstract class MyColor extends Color6 @MongoTypeHint("red") case object Red extends MyColor @MongoTypeHint("custom") case class Custom(rgb: String) extends MyColor + val format = deriveMongoFormat[Color6] } sealed trait Color7 diff --git a/util/src/main/scala-3/VectorUtils.scala b/util/src/main/scala-3/VectorUtils.scala new file mode 100644 index 00000000..5b9fe943 --- /dev/null +++ b/util/src/main/scala-3/VectorUtils.scala @@ -0,0 +1,19 @@ +package io.sphere.util + +object VectorUtils { + + extension [A](vector: Vector[A]) { + + // toMap by default will remove all but one of the key value pairs in case of duplicate keys + def toMapWithNoDuplicateKeys[K, V](using A <:< (K, V)): Map[K, V] = { + val duplicateKeys = + vector.groupBy(_._1).collect { case (key, values) if values.size >= 2 => key } + if (duplicateKeys.nonEmpty) + throw new Exception( + s"Cannot construct Map because the following keys are duplicates: ${duplicateKeys.mkString(", ")}") + else + vector.toMap + } + } + +} From a15a634634334a02f5c65514ff709def95916035 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 1 May 2025 20:41:37 +0200 Subject: [PATCH 101/142] Enable nested trait deriving --- .../io/sphere/mongo/format/MongoFormat.scala | 52 +++++++++++------- .../mongo/generic/SumTypesDerivingSpec.scala | 54 +++++++++++++------ 2 files changed, 71 insertions(+), 35 deletions(-) diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala index bbcf193d..4e34393b 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala @@ -3,11 +3,13 @@ package io.sphere.mongo.format import com.mongodb.BasicDBObject import io.sphere.mongo.generic.{AnnotationReader, Field} import io.sphere.util.VectorUtils.* +import org.bson.BSONObject import org.bson.types.ObjectId import java.util.UUID import java.util.regex.Pattern import scala.deriving.Mirror +import scala.util.{Success, Try} type SimpleMongoType = UUID | String | ObjectId | Short | Int | Long | Float | Double | Boolean | Pattern @@ -28,6 +30,10 @@ trait MongoFormat[A] extends Serializable { trait TraitMongoFormat[A] extends MongoFormat[A] { val subTypeNames: Vector[String] val typeDiscriminator: String + + def attemptWrite(a: A): Try[Any] = Try(toMongoValue(a)) + + def attemptRead(bson: BSONObject): Try[A] = Try(fromMongoValue(bson)) } object TraitMongoFormat { @@ -85,38 +91,44 @@ object MongoFormat { val typeHintMap = traitMetaData.subTypeTypeHints val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) val formatters = summonFormatters[mirrorOfSum.MirroredElemTypes] - val names = constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector + val subTypeNames = constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector .asInstanceOf[Vector[String]] - val formattersByTypeName = names - .zip(formatters) - .flatMap { case kv @ (name, formatter) => + val pairedFormatterWithSubtypeName = subTypeNames.zip(formatters) + val (caseClassFormatterList, traitFormatters) = pairedFormatterWithSubtypeName.partitionMap { + case kv @ (name, formatter) => formatter match { - case traitFormatter: TraitMongoFormat[_] => - traitFormatter.subTypeNames.map(_ -> formatter) - case _ => Vector(kv) + case traitFormatter: TraitMongoFormat[_] => Right(traitFormatter) + case _ => Left(kv) } - } - .toMapWithNoDuplicateKeys + } + val caseClassFormatters = caseClassFormatterList.toMap TraitMongoFormat.instance( toMongo = { a => - // we never get a trait here, only classes, it's safe to assume Product - val originalTypeName = a.asInstanceOf[Product].productPrefix - val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) - val bson = - formattersByTypeName(originalTypeName).toMongoValue(a).asInstanceOf[BasicDBObject] - bson.put(traitMetaData.typeDiscriminator, typeName) - bson + traitFormatters.view.map(_.attemptWrite(a)).find(_.isSuccess).map(_.get) match { + case Some(bson) => bson + case None => + val originalTypeName = a.asInstanceOf[Product].productPrefix + val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) + val bson = + caseClassFormatters(originalTypeName).toMongoValue(a).asInstanceOf[BasicDBObject] + bson.put(traitMetaData.typeDiscriminator, typeName) + bson + } }, fromMongo = { case bson: BasicDBObject => - val typeName = bson.get(traitMetaData.typeDiscriminator).asInstanceOf[String] - val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - formattersByTypeName(originalTypeName).fromMongoValue(bson).asInstanceOf[A] + traitFormatters.view.map(_.attemptRead(bson)).find(_.isSuccess).map(_.get) match { + case Some(a) => a.asInstanceOf[A] + case None => + val typeName = bson.get(traitMetaData.typeDiscriminator).asInstanceOf[String] + val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) + caseClassFormatters(originalTypeName).fromMongoValue(bson).asInstanceOf[A] + } case x => throw new Exception(s"BsonObject is expected for a Trait subtype, instead got $x") }, - subTypes = names, + subTypes = subTypeNames, typeDiscr = traitMetaData.typeDiscriminator ) } diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala index d7b83365..e71c3e6b 100644 --- a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala +++ b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala @@ -37,20 +37,29 @@ class SumTypesDerivingSpec extends AnyWordSpec with Matchers { check(Color4.format, Color4.Custom("2356"), dbObj("color" -> "custom", "rgb" -> "2356")) } - "ignore @MongoTypeHintField on subtypes if it's present on the trait" in { + "ignore @MongoTypeHintField on case classes" in { check(Color5.format, Color5.Red, dbObj("color" -> "red")) - check(Color5.format, Color5.Custom("123"), dbObj("color" -> "custom", "rgb" -> "123")) } - "not allow specifying different custom field on intermediate level" in { - // TODO make this more sophisticated in the future - // The color is Red because the top level trait doesn't read the annotations of children of it's children - check(Color6.format, Color6.Red, dbObj("color-custom" -> "red", "color" -> "Red")) + "nested trait 1: no duplicate names, 2 type discriminators" in { + check(Color6.format, Color6.Red, dbObj("custom-color" -> "mapped-red")) + } + + "nested trait 2: duplicate names, 1 type discriminator" in pendingUntilFixed { + // This doesn't fail currently, because we don't validate it. + val format = deriveMongoFormat[Color6a] + check(format, Color6b.Red, dbObj("type" -> "Red")) + check(format, Color6c.Red, dbObj("type" -> "Red")) + } + "nested trait 3: 2 duplicate names, 2 type discriminators" in { + val format = deriveMongoFormat[Color6e] + check(format, Color6g.Red, dbObj("type" -> "Red")) + check(format, Color6f.Red, dbObj("color-custom" -> "Red")) } - "use intermediate level" in { + "nested trait 4: no duplicates, 1 type discriminator" in { Color7.format } @@ -60,16 +69,10 @@ class SumTypesDerivingSpec extends AnyWordSpec with Matchers { check(Color8.Custom.format, Color8.Custom("2356"), dbObj("rgb" -> "2356")) // unless annotated - check( Color8.format, Color8.CustomAnnotated("2356"), dbObj("type" -> "CustomAnnotated", "rgb" -> "2356")) -// TODO should this be a thing? -// check( -// Color8.CustomAnnotated.format, -// Color8.CustomAnnotated("2356"), -// dbObj("type" -> "CustomAnnotated", "rgb" -> "2356")) } "use default values if custom values are empty" in { @@ -168,15 +171,36 @@ object SumTypesDerivingSpec { @MongoTypeHintField("color") sealed trait Color6 object Color6 { - @MongoTypeHintField("color-custom") + @MongoTypeHintField("custom-color") sealed abstract class MyColor extends Color6 - @MongoTypeHint("red") + @MongoTypeHint("mapped-red") case object Red extends MyColor @MongoTypeHint("custom") case class Custom(rgb: String) extends MyColor val format = deriveMongoFormat[Color6] } + sealed trait Color6a + sealed trait Color6b extends Color6a + object Color6b { + case object Red extends Color6b + } + sealed trait Color6c extends Color6a + object Color6c { + case object Red extends Color6c + } + + sealed trait Color6e + @MongoTypeHintField("color-custom") + sealed trait Color6f extends Color6e + object Color6f { + case object Red extends Color6f + } + sealed trait Color6g extends Color6e + object Color6g { + case object Red extends Color6g + } + sealed trait Color7 sealed trait Color7a extends Color7 object Color7 { From 06cea4849f39e61989c1f986aec6728db5cd580d Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 1 May 2025 20:53:52 +0200 Subject: [PATCH 102/142] Format --- .../generic/DeriveFromJSON.scala | 2 +- .../io/sphere/mongo/format/MongoFormat.scala | 20 +++++++------------ .../mongo/generic/SumTypesDerivingSpec.scala | 5 ++++- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala index ed05c3aa..37343301 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala @@ -3,7 +3,7 @@ package io.sphere.json.generic import cats.data.Validated import cats.syntax.traverse.* import io.sphere.json.field -import io.sphere.json.generic.{AnnotationReader, TypeMetaData, Field, TraitMetaData} +import io.sphere.json.generic.{AnnotationReader, Field, TraitMetaData, TypeMetaData} import org.json4s.JsonAST.* import org.json4s.DefaultReaders.StringReader import org.json4s.{jvalue2monadic, jvalue2readerSyntax} diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala index 4e34393b..d6424575 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala @@ -28,24 +28,20 @@ trait MongoFormat[A] extends Serializable { * easier */ trait TraitMongoFormat[A] extends MongoFormat[A] { - val subTypeNames: Vector[String] - val typeDiscriminator: String - + // This approach is somewhat slow, the reason I chose to implement it like this is because: + // 1. We don't have nested trait structures anyway, the scala 2 version didn't even work for this case. + // So this is more of a proof of concept feature + // 2. I didn't find a way to check types runtime when you have a nested trait hierarchy, because of erasure. + // So this instance wouldn't know if it's really dealing with one of its subtypes or with another trait's subtype. def attemptWrite(a: A): Try[Any] = Try(toMongoValue(a)) def attemptRead(bson: BSONObject): Try[A] = Try(fromMongoValue(bson)) } object TraitMongoFormat { - def instance[A]( - fromMongo: Any => A, - toMongo: A => Any, - subTypes: Vector[String], - typeDiscr: String): TraitMongoFormat[A] = new { + def instance[A](fromMongo: Any => A, toMongo: A => Any): TraitMongoFormat[A] = new { override def toMongoValue(a: A): Any = toMongo(a) override def fromMongoValue(mongoType: Any): A = fromMongo(mongoType) - override val subTypeNames: Vector[String] = subTypes - override val typeDiscriminator: String = typeDiscr } } @@ -127,9 +123,7 @@ object MongoFormat { } case x => throw new Exception(s"BsonObject is expected for a Trait subtype, instead got $x") - }, - subTypes = subTypeNames, - typeDiscr = traitMetaData.typeDiscriminator + } ) } diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala index e71c3e6b..41ab1daa 100644 --- a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala +++ b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala @@ -60,7 +60,9 @@ class SumTypesDerivingSpec extends AnyWordSpec with Matchers { } "nested trait 4: no duplicates, 1 type discriminator" in { - Color7.format + check(Color7.format, Color7.Red, dbObj("type" -> "Red")) + check(Color7.format, Color7.Blue, dbObj("type" -> "Blue")) + check(Color7.format, Color7.Custom("234"), dbObj("rgb" -> "234", "type" -> "Custom")) } "do not use sealed trait info when using a case class directly" in { @@ -206,6 +208,7 @@ object SumTypesDerivingSpec { object Color7 { case object Red extends Color7a case class Custom(rgb: String) extends Color7a + case object Blue extends Color7 def format = deriveMongoFormat[Color7] } From 0738ba6c0860f9fbd726eb7de469cba5a66167f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Brunk?= Date: Fri, 2 May 2025 14:07:32 +0200 Subject: [PATCH 103/142] Remove outdated experimental magnolia derivation --- build.sbt | 7 - .../dependencies.sbt | 3 - .../sphere/mongo/generic/MongoEmbedded.scala | 5 - .../io/sphere/mongo/generic/MongoIgnore.scala | 5 - .../io/sphere/mongo/generic/MongoKey.scala | 5 - .../sphere/mongo/generic/MongoTypeHint.scala | 5 - .../mongo/generic/MongoTypeHintField.scala | 10 - .../io/sphere/mongo/generic/package.scala | 280 ------------------ .../io/sphere/mongo/SerializationTest.scala | 61 ---- .../mongo/format/OptionMongoFormatSpec.scala | 96 ------ .../mongo/generic/DefaultValuesSpec.scala | 34 --- .../mongo/generic/DeriveMongoformatSpec.scala | 121 -------- .../mongo/generic/MongoEmbeddedSpec.scala | 145 --------- .../sphere/mongo/generic/MongoKeySpec.scala | 48 --- ...goTypeHintFieldWithAbstractClassSpec.scala | 57 ---- ...ongoTypeHintFieldWithSealedTraitSpec.scala | 57 ---- .../mongo/generic/SumTypesDerivingSpec.scala | 172 ----------- 17 files changed, 1111 deletions(-) delete mode 100644 mongo/mongo-derivation-magnolia/dependencies.sbt delete mode 100644 mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/MongoEmbedded.scala delete mode 100644 mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/MongoIgnore.scala delete mode 100644 mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/MongoKey.scala delete mode 100644 mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/MongoTypeHint.scala delete mode 100644 mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/MongoTypeHintField.scala delete mode 100644 mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/package.scala delete mode 100644 mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/SerializationTest.scala delete mode 100644 mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala delete mode 100644 mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala delete mode 100644 mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/DeriveMongoformatSpec.scala delete mode 100644 mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala delete mode 100644 mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala delete mode 100644 mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala delete mode 100644 mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala delete mode 100644 mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala diff --git a/build.sbt b/build.sbt index 5c9dbaca..1ca34117 100644 --- a/build.sbt +++ b/build.sbt @@ -26,7 +26,6 @@ ThisBuild / githubWorkflowBuild := Seq( "sphere-mongo/test", "sphere-mongo-core/test", "sphere-mongo-derivation/test", - "sphere-mongo-derivation-magnolia/test", "benchmarks/test" ), name = Some("Build Scala 2 project"), @@ -107,7 +106,6 @@ lazy val `sphere-libs` = project `sphere-mongo`, `sphere-mongo-core`, `sphere-mongo-derivation`, - `sphere-mongo-derivation-magnolia`, `benchmarks` ) @@ -158,11 +156,6 @@ lazy val `sphere-mongo-derivation` = project .settings(crossScalaVersions := Seq(scala213)) .dependsOn(`sphere-mongo-core`) -lazy val `sphere-mongo-derivation-magnolia` = project - .in(file("./mongo/mongo-derivation-magnolia")) - .settings(standardSettings: _*) - .settings(crossScalaVersions := Seq(scala213)) - .dependsOn(`sphere-mongo-core`) lazy val `sphere-mongo` = project .in(file("./mongo")) diff --git a/mongo/mongo-derivation-magnolia/dependencies.sbt b/mongo/mongo-derivation-magnolia/dependencies.sbt deleted file mode 100644 index 291cdeae..00000000 --- a/mongo/mongo-derivation-magnolia/dependencies.sbt +++ /dev/null @@ -1,3 +0,0 @@ -libraryDependencies ++= Seq( - "com.propensive" %% "magnolia" % "0.17.0" -) diff --git a/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/MongoEmbedded.scala b/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/MongoEmbedded.scala deleted file mode 100644 index cf2aa20a..00000000 --- a/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/MongoEmbedded.scala +++ /dev/null @@ -1,5 +0,0 @@ -package io.sphere.mongo.generic - -import scala.annotation.StaticAnnotation - -class MongoEmbedded extends StaticAnnotation diff --git a/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/MongoIgnore.scala b/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/MongoIgnore.scala deleted file mode 100644 index b528da47..00000000 --- a/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/MongoIgnore.scala +++ /dev/null @@ -1,5 +0,0 @@ -package io.sphere.mongo.generic - -import scala.annotation.StaticAnnotation - -class MongoIgnore extends StaticAnnotation diff --git a/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/MongoKey.scala b/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/MongoKey.scala deleted file mode 100644 index 8fe8c1cc..00000000 --- a/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/MongoKey.scala +++ /dev/null @@ -1,5 +0,0 @@ -package io.sphere.mongo.generic - -import scala.annotation.StaticAnnotation - -case class MongoKey(value: String) extends StaticAnnotation diff --git a/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/MongoTypeHint.scala b/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/MongoTypeHint.scala deleted file mode 100644 index a56dde8d..00000000 --- a/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/MongoTypeHint.scala +++ /dev/null @@ -1,5 +0,0 @@ -package io.sphere.mongo.generic - -import scala.annotation.StaticAnnotation - -case class MongoTypeHint(value: String) extends StaticAnnotation diff --git a/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/MongoTypeHintField.scala b/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/MongoTypeHintField.scala deleted file mode 100644 index 77f54dcb..00000000 --- a/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/MongoTypeHintField.scala +++ /dev/null @@ -1,10 +0,0 @@ -package io.sphere.mongo.generic - -import scala.annotation.StaticAnnotation - -case class MongoTypeHintField(value: String = MongoTypeHintField.defaultValue) - extends StaticAnnotation - -object MongoTypeHintField { - final val defaultValue: String = "type" -} diff --git a/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/package.scala b/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/package.scala deleted file mode 100644 index 019a1f85..00000000 --- a/mongo/mongo-derivation-magnolia/src/main/scala/io/sphere/mongo/generic/package.scala +++ /dev/null @@ -1,280 +0,0 @@ -package io.sphere.mongo - -import com.mongodb.{BasicDBObject, DBObject} -import io.sphere.mongo.format.{MongoFormat, MongoNothing, toMongo} -import io.sphere.util.{Logging, Memoizer} -import magnolia._ -import org.bson.BSONObject - -import scala.language.experimental.macros - -package object generic extends Logging { - - /** Creates a MongoFormat instance for an Enumeration type that encodes the `toString` - * representations of the enumeration values. - */ - def mongoEnum(e: Enumeration): MongoFormat[e.Value] = new MongoFormat[e.Value] { - def toMongoValue(a: e.Value): Any = a.toString - def fromMongoValue(any: Any): e.Value = e.withName(any.asInstanceOf[String]) - } - - type Typeclass[T] = MongoFormat[T] - - def deriveMongoFormat[T]: MongoFormat[T] = macro Magnolia.gen[T] - - private val addNoTypeHint: DBObject => Unit = Function.const(()) - - // derive for a case class - def combine[T](caseClass: CaseClass[MongoFormat, T]): MongoFormat[T] = new MongoFormat[T] { - private val mongoClass = getMongoClassMeta(caseClass) - private val _fields = mongoClass.fields - private val _withTypeHint = mongoClass.typeHint.isDefined - private val addTypeHint: DBObject => Unit = - mongoClass.typeHint.fold(addNoTypeHint)(th => dbo => dbo.put(th.field, th.value)) - - override def toMongoValue(r: T): Any = { - val dbo = new BasicDBObject - if (_withTypeHint) addTypeHint(dbo) - - var i = 0 - caseClass.parameters.foreach { p => - writeField(dbo, _fields(i), p.dereference(r))(p.typeclass) - i += 1 - } - dbo - } - - override def fromMongoValue(any: Any): T = any match { - case dbo: DBObject => - var i = -1 - val fieldValues: Seq[Any] = caseClass.parameters.map { p => - i += 1 - readField(_fields(i), dbo)(p.typeclass) - } - caseClass.rawConstruct(fieldValues) - case _ => sys.error("Deserialization failed. DBObject expected.") - } - - override val fields: Set[String] = calculateFields() - private def calculateFields(): Set[String] = { - val builder = Set.newBuilder[String] - var i = 0 - caseClass.parameters.foreach { p => - val f = _fields(i) - if (!f.ignored) { - if (f.embedded) - builder ++= p.typeclass.fields - else - builder += f.name - } - i += 1 - } - builder.result() - } - } - - // derive for a sealed trait - def dispatch[T](sealedTrait: SealedTrait[MongoFormat, T]): MongoFormat[T] = new MongoFormat[T] { - - private val allSelectors = sealedTrait.subtypes.map { subType => - typeSelector(subType) - } - private val readMapBuilder = Map.newBuilder[String, TypeSelector[_]] - private val writeMapBuilder = Map.newBuilder[TypeName, TypeSelector[_]] - allSelectors.foreach { s => - readMapBuilder += (s.typeValue -> s) - writeMapBuilder += (s.subType.typeName -> s) - } - private val readMap = readMapBuilder.result() - private val writeMap = writeMapBuilder.result() - - private val typeField = sealedTrait.annotations - .collectFirst { case a: MongoTypeHintField => - a.value - } - .getOrElse(defaultTypeFieldName) - - override def toMongoValue(t: T): Any = - sealedTrait.dispatch(t) { subtype => - writeMap.get(subtype.typeName) match { - case None => new BasicDBObject(typeField, defaultTypeValue(subtype.typeName)) - case Some(w) => - subtype.typeclass.toMongoValue(subtype.cast(t)) match { - case dbo: BSONObject => - findTypeValue(dbo, typeField) match { - case Some(_) => dbo - case None => - dbo.put(typeField, w.typeValue) - dbo - } - case _ => throw new Exception("Excepted 'BSONObject'") - } - } - } - - override def fromMongoValue(any: Any): T = - any match { - case dbo: BSONObject => - findTypeValue(dbo, typeField) match { - case Some(t) => - readMap.get(t) match { - case Some(r) => r.subType.typeclass.fromMongoValue(dbo).asInstanceOf[T] - case None => - sys.error("Invalid type value '" + t + "' in DBObject '%s'.".format(dbo)) - } - case None => - sys.error("Missing type field '" + typeField + "' in DBObject '%s'.".format(dbo)) - } - case _ => sys.error("DBObject expected.") - } - } - - private val defaultTypeFieldName: String = MongoTypeHintField.defaultValue - - private case class MongoClassMeta( - typeHint: Option[MongoClassMeta.TypeHint], - fields: IndexedSeq[MongoFieldMeta]) - private object MongoClassMeta { - case class TypeHint(field: String, value: String) - } - private case class MongoFieldMeta( - name: String, - default: Option[Any] = None, - embedded: Boolean = false, - ignored: Boolean = false - ) - - private val getMongoClassMeta = - new Memoizer[CaseClass[MongoFormat, _], MongoClassMeta](caseClass => { - log.trace("Initializing Mongo metadata for %s".format(caseClass.typeName.full)) - - val annotations = caseClass.annotations - - val typeHintFieldAnnot: Option[MongoTypeHintField] = annotations.collectFirst { - case h: MongoTypeHintField => h - } - val typeHintAnnot: Option[generic.MongoTypeHint] = annotations.collectFirst { - case h: generic.MongoTypeHint => h - } - val typeField = typeHintFieldAnnot.map(_.value) - val typeValue = typeHintAnnot.map(hintVal(caseClass.typeName)) - - MongoClassMeta( - typeHint = (typeField, typeValue) match { - case (Some(field), Some(hint)) => Some(MongoClassMeta.TypeHint(field, hint)) - case (None, Some(hint)) => Some(MongoClassMeta.TypeHint(defaultTypeFieldName, hint)) - case (Some(field), None) => - Some(MongoClassMeta.TypeHint(field, defaultTypeValue(caseClass.typeName))) - case (None, None) => None - }, - fields = getMongoFieldMeta(caseClass) - ) - }) - - private val getMongoClassMetaFromSubType = - new Memoizer[Subtype[MongoFormat, _], MongoClassMeta](subType => { - log.trace("Initializing Mongo metadata for %s".format(subType.typeName.full)) - - val annotations = subType.annotations - - val typeHintFieldAnnot: Option[MongoTypeHintField] = annotations.collectFirst { - case h: MongoTypeHintField => h - } - val typeHintAnnot: Option[generic.MongoTypeHint] = annotations.collectFirst { - case h: generic.MongoTypeHint => h - } - val typeField = typeHintFieldAnnot.map(_.value) - val typeValue = typeHintAnnot.map(hintVal(subType.typeName)) - - MongoClassMeta( - typeHint = (typeField, typeValue) match { - case (Some(field), Some(hint)) => Some(MongoClassMeta.TypeHint(field, hint)) - case (None, Some(hint)) => Some(MongoClassMeta.TypeHint(defaultTypeFieldName, hint)) - case (Some(field), None) => - Some(MongoClassMeta.TypeHint(field, defaultTypeValue(subType.typeName))) - case (None, None) => None - }, - fields = IndexedSeq[MongoFieldMeta]() - ) - }) - - private def getMongoFieldMeta(caseClass: CaseClass[MongoFormat, _]): IndexedSeq[MongoFieldMeta] = - caseClass.parameters.map { p => - val annotations = p.annotations - val name = annotations - .collectFirst { case h: MongoKey => - h - } - .fold(p.label)(_.value) - val embedded = annotations.exists { - case _: MongoEmbedded => true - case _ => false - } - val ignored = annotations.exists { - case _: MongoIgnore => true - case _ => false - } - if (ignored && p.default.isEmpty) { - throw new Exception("Ignored Mongo field '%s' must have a default value.".format(p.label)) - } - MongoFieldMeta(name, p.default, embedded, ignored) - }.toIndexedSeq - - private def writeField[A: MongoFormat](dbo: DBObject, field: MongoFieldMeta, e: A): Unit = - if (!field.ignored) { - if (field.embedded) - toMongo(e) match { - case dbo2: DBObject => dbo.putAll(dbo2) - case MongoNothing => () - case x => dbo.put(field.name, x) - } - else - toMongo(e) match { - case MongoNothing => () - case x => dbo.put(field.name, x) - } - } - - private def readField[A: MongoFormat](f: MongoFieldMeta, dbo: DBObject): A = { - val mf = MongoFormat[A] - def default = f.default.asInstanceOf[Option[A]].orElse(mf.default) - if (f.ignored) - default.getOrElse { - throw new Exception("Missing default for ignored field '%s'.".format(f.name)) - } - else if (f.embedded) mf.fromMongoValue(dbo) - else { - val value = dbo.get(f.name) - if (value != null) mf.fromMongoValue(value) - else { - default.getOrElse { - throw new Exception("Missing required field '%s' on deserialization.".format(f.name)) - } - } - } - } - - private def findTypeValue(dbo: BSONObject, typeField: String): Option[String] = - Option(dbo.get(typeField)).map(_.toString) - - private case class TypeSelector[A]( - val typeField: String, - val typeValue: String, - subType: Subtype[MongoFormat, A]) - - private def typeSelector[A](subType: Subtype[MongoFormat, A]): TypeSelector[A] = { - val (typeField, typeValue) = getMongoClassMetaFromSubType(subType).typeHint match { - case Some(hint) => (hint.field, hint.value) - case None => (defaultTypeFieldName, defaultTypeValue(subType.typeName)) - } - new TypeSelector[A](typeField, typeValue, subType) - } - - private def hintVal(typeName: TypeName)(h: generic.MongoTypeHint): String = - if (h.value.trim.isEmpty) defaultTypeValue(typeName) - else h.value - - private def defaultTypeValue(typeName: TypeName): String = - typeName.short.replace("$", "") - -} diff --git a/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/SerializationTest.scala b/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/SerializationTest.scala deleted file mode 100644 index e061e75e..00000000 --- a/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/SerializationTest.scala +++ /dev/null @@ -1,61 +0,0 @@ -package io.sphere.mongo - -import com.mongodb.{BasicDBObject, DBObject} -import org.scalatest.matchers.must.Matchers -import io.sphere.mongo.format.MongoFormat -import io.sphere.mongo.format.DefaultMongoFormats._ -import org.scalatest.wordspec.AnyWordSpec - -object SerializationTest { - case class Something(a: Option[Int], b: Int = 2) - - object Color extends Enumeration { - val Blue, Red, Yellow = Value - } -} - -class SerializationTest extends AnyWordSpec with Matchers { - import SerializationTest._ - - "mongoProduct" must { - "deserialize mongo object" in { - val dbo = new BasicDBObject() - dbo.put("a", Integer.valueOf(3)) - dbo.put("b", Integer.valueOf(4)) - - val mongoFormat: MongoFormat[Something] = io.sphere.mongo.generic.deriveMongoFormat - val something = mongoFormat.fromMongoValue(dbo) - something must be(Something(Some(3), 4)) - } - - "generate a format that serializes optional fields with value None as BSON objects without that field" in { - val testFormat: MongoFormat[Something] = io.sphere.mongo.generic.deriveMongoFormat[Something] - val serializedObject = testFormat.toMongoValue(Something(None, 1)).asInstanceOf[DBObject] - serializedObject.keySet().contains("b") must be(true) - serializedObject.keySet().contains("a") must be(false) - } - - "generate a format that use default values" in { - val dbo = new BasicDBObject() - dbo.put("a", Integer.valueOf(3)) - - val mongoFormat: MongoFormat[Something] = io.sphere.mongo.generic.deriveMongoFormat - val something = mongoFormat.fromMongoValue(dbo) - something must be(Something(Some(3), 2)) - } - } - - "mongoEnum" must { - "serialize and deserialize enums" in { - val mongo: MongoFormat[Color.Value] = generic.mongoEnum(Color) - - // mongo java driver knows how to encode/decode Strings - val serializedObject = mongo.toMongoValue(Color.Red).asInstanceOf[String] - serializedObject must be("Red") - - val enumValue = mongo.fromMongoValue(serializedObject) - enumValue must be(Color.Red) - } - } - -} diff --git a/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala b/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala deleted file mode 100644 index f8485610..00000000 --- a/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala +++ /dev/null @@ -1,96 +0,0 @@ -package io.sphere.mongo.format - -import io.sphere.mongo.generic._ -import org.scalatest.OptionValues -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec -import io.sphere.mongo.MongoUtils._ -import DefaultMongoFormats._ - -object OptionMongoFormatSpec { - - case class SimpleClass(value1: String, value2: Int) - - object SimpleClass { - implicit val mongo: MongoFormat[SimpleClass] = deriveMongoFormat - } - - case class ComplexClass(name: String, simpleClass: Option[SimpleClass]) - - object ComplexClass { - implicit val mongo: MongoFormat[ComplexClass] = deriveMongoFormat - } - -} - -class OptionMongoFormatSpec extends AnyWordSpec with Matchers with OptionValues { - import OptionMongoFormatSpec._ - - "MongoFormat[Option[_]]" should { - "handle presence of all fields" in { - val dbo = dbObj( - "value1" -> "a", - "value2" -> 45 - ) - val result = MongoFormat[Option[SimpleClass]].fromMongoValue(dbo) - result.value.value1 mustEqual "a" - result.value.value2 mustEqual 45 - } - - "handle presence of all fields mixed with ignored fields" in { - val dbo = dbObj( - "value1" -> "a", - "value2" -> 45, - "value3" -> "b" - ) - val result = MongoFormat[Option[SimpleClass]].fromMongoValue(dbo) - result.value.value1 mustEqual "a" - result.value.value2 mustEqual 45 - } - - "handle presence of not all the fields" in { - val dbo = dbObj("value1" -> "a") - an[Exception] mustBe thrownBy(MongoFormat[Option[SimpleClass]].fromMongoValue(dbo)) - } - - "handle absence of all fields" in { - val dbo = dbObj() - val result = MongoFormat[Option[SimpleClass]].fromMongoValue(dbo) - result mustEqual None - } - - "handle absence of all fields mixed with ignored fields" in { - val dbo = dbObj("value3" -> "a") - val result = MongoFormat[Option[SimpleClass]].fromMongoValue(dbo) - result mustEqual None - } - - "consider all fields if the data type does not impose any restriction" in { - val dbo = dbObj( - "key1" -> "value1", - "key2" -> "value2" - ) - val expected = Map("key1" -> "value1", "key2" -> "value2") - val result = MongoFormat[Map[String, String]].fromMongoValue(dbo) - result mustEqual expected - - val maybeResult = MongoFormat[Option[Map[String, String]]].fromMongoValue(dbo) - maybeResult.value mustEqual expected - } - - "parse optional element" in { - val dbo = dbObj( - "name" -> "ze name", - "simpleClass" -> dbObj( - "value1" -> "value1", - "value2" -> 42 - ) - ) - val result = MongoFormat[ComplexClass].fromMongoValue(dbo) - result.simpleClass.value.value1 mustEqual "value1" - result.simpleClass.value.value2 mustEqual 42 - - MongoFormat[ComplexClass].toMongoValue(result) mustEqual dbo - } - } -} diff --git a/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala b/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala deleted file mode 100644 index 03e7dd2d..00000000 --- a/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala +++ /dev/null @@ -1,34 +0,0 @@ -package io.sphere.mongo.generic - -import io.sphere.mongo.MongoUtils._ -import io.sphere.mongo.format.DefaultMongoFormats._ -import io.sphere.mongo.format.MongoFormat -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class DefaultValuesSpec extends AnyWordSpec with Matchers { - import DefaultValuesSpec._ - - "deriving MongoFormat" must { - "handle default values" in { - val dbo = dbObj() - val test = MongoFormat[Test].fromMongoValue(dbo) - test.value1 must be("hello") - test.value2 must be(None) - test.value3 must be(None) - test.value4 must be(Some("hi")) - } - } -} - -object DefaultValuesSpec { - case class Test( - value1: String = "hello", - value2: Option[String], - value3: Option[String] = None, - value4: Option[String] = Some("hi") - ) - object Test { - implicit val mongo: MongoFormat[Test] = deriveMongoFormat - } -} diff --git a/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/DeriveMongoformatSpec.scala b/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/DeriveMongoformatSpec.scala deleted file mode 100644 index 201145f9..00000000 --- a/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/DeriveMongoformatSpec.scala +++ /dev/null @@ -1,121 +0,0 @@ -package io.sphere.mongo.generic - -import io.sphere.mongo.format.MongoFormat -import io.sphere.mongo.format._ -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec -import io.sphere.mongo.format.DefaultMongoFormats._ -import io.sphere.mongo.MongoUtils._ - -class DeriveMongoformatSpec extends AnyWordSpec with Matchers { - import DeriveMongoformatSpec._ - - "deriving MongoFormat" must { - "read normal singleton values" in { - val user = fromMongo[UserWithPicture]( - dbObj( - "userId" -> "foo-123", - "pictureSize" -> dbObj("type" -> "Medium"), - "pictureUrl" -> "http://example.com")) - - user must be(UserWithPicture("foo-123", Medium, "http://example.com")) - } - - "read custom singleton values" in { - val user = fromMongo[UserWithPicture]( - dbObj( - "userId" -> "foo-123", - "pictureSize" -> dbObj("type" -> "bar", "width" -> 23, "height" -> 30), - "pictureUrl" -> "http://example.com")) - - user must be(UserWithPicture("foo-123", Custom(23, 30), "http://example.com")) - } - - "fail to read if singleton value is unknown" in { - a[Exception] must be thrownBy fromMongo[UserWithPicture]( - dbObj( - "userId" -> "foo-123", - "pictureSize" -> dbObj("type" -> "Unknown"), - "pictureUrl" -> "http://example.com")) - } - - "write normal singleton values" in { - val dbo = toMongo[UserWithPicture](UserWithPicture("foo-123", Medium, "http://example.com")) - dbo must be( - dbObj( - "userId" -> "foo-123", - "pictureSize" -> dbObj("type" -> "Medium"), - "pictureUrl" -> "http://example.com")) - } - - "write custom singleton values" in { - val dbo = - toMongo[UserWithPicture](UserWithPicture("foo-123", Custom(23, 30), "http://example.com")) - dbo must be( - dbObj( - "userId" -> "foo-123", - "pictureSize" -> dbObj("type" -> "bar", "width" -> 23, "height" -> 30), - "pictureUrl" -> "http://example.com")) - } - - "write and consequently read, which must produce the original value" in { - val originalUser = UserWithPicture("foo-123", Medium, "http://exmple.com") - val newUser = fromMongo[UserWithPicture](toMongo[UserWithPicture](originalUser)) - - newUser must be(originalUser) - } - - "read and write sealed trait with only one subtype" in { - val dbo = dbObj( - "userId" -> "foo-123", - "pictureSize" -> dbObj("type" -> "Medium"), - "pictureUrl" -> "http://example.com", - "access" -> dbObj("type" -> "Authorized", "project" -> "internal") - ) - val user = fromMongo[UserWithPicture](dbo) - - user must be( - UserWithPicture( - "foo-123", - Medium, - "http://example.com", - Some(Access.Authorized("internal")))) - val newDbo = toMongo[UserWithPicture](user) - newDbo must be(dbo) - - val newUser = fromMongo[UserWithPicture](newDbo) - newUser must be(user) - } - } -} - -object DeriveMongoformatSpec { - sealed trait PictureSize - case object Small extends PictureSize - case object Medium extends PictureSize - case object Big extends PictureSize - @MongoTypeHint(value = "bar") - case class Custom(width: Int, height: Int) extends PictureSize - - object PictureSize { - implicit val mongo: MongoFormat[PictureSize] = deriveMongoFormat[PictureSize] - } - - sealed trait Access - object Access { - // only one sub-type - case class Authorized(project: String) extends Access - - implicit val mongo: MongoFormat[Access] = deriveMongoFormat - } - - case class UserWithPicture( - userId: String, - pictureSize: PictureSize, - pictureUrl: String, - access: Option[Access] = None) - - object UserWithPicture { - implicit val mongo: MongoFormat[UserWithPicture] = deriveMongoFormat - } -} diff --git a/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala b/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala deleted file mode 100644 index 12d1b52a..00000000 --- a/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala +++ /dev/null @@ -1,145 +0,0 @@ -package io.sphere.mongo.generic - -import io.sphere.mongo.format.MongoFormat -import org.scalatest.OptionValues -import org.scalatest.matchers.must.Matchers -import io.sphere.mongo.format.DefaultMongoFormats._ -import io.sphere.mongo.MongoUtils._ -import org.scalatest.wordspec.AnyWordSpec - -import scala.util.Try - -object MongoEmbeddedSpec { - case class Embedded(value1: String, @MongoKey("_value2") value2: Int) - - object Embedded { - implicit val mongo: MongoFormat[Embedded] = deriveMongoFormat - } - - case class Test1(name: String, @MongoEmbedded embedded: Embedded) - - object Test1 { - implicit val mongo: MongoFormat[Test1] = deriveMongoFormat - } - - case class Test2(name: String, @MongoEmbedded embedded: Option[Embedded] = None) - - object Test2 { - implicit val mongo: MongoFormat[Test2] = deriveMongoFormat - } - - case class Test3( - @MongoIgnore name: String = "default", - @MongoEmbedded embedded: Option[Embedded] = None) - - object Test3 { - implicit val mongo: MongoFormat[Test3] = deriveMongoFormat - } - - case class SubTest4(@MongoEmbedded embedded: Embedded) - object SubTest4 { - implicit val mongo: MongoFormat[SubTest4] = deriveMongoFormat - } - - case class Test4(subField: Option[SubTest4] = None) - object Test4 { - implicit val mongo: MongoFormat[Test4] = deriveMongoFormat - } -} - -class MongoEmbeddedSpec extends AnyWordSpec with Matchers with OptionValues { - import MongoEmbeddedSpec._ - - "MongoEmbedded" should { - "flatten the db object in one object" in { - val dbo = dbObj( - "name" -> "ze name", - "value1" -> "ze value1", - "_value2" -> 45 - ) - val test1 = MongoFormat[Test1].fromMongoValue(dbo) - test1.name mustEqual "ze name" - test1.embedded.value1 mustEqual "ze value1" - test1.embedded.value2 mustEqual 45 - - val result = MongoFormat[Test1].toMongoValue(test1) - result mustEqual dbo - } - - "validate that the db object contains all needed fields" in { - val dbo = dbObj( - "name" -> "ze name", - "value1" -> "ze value1" - ) - Try(MongoFormat[Test1].fromMongoValue(dbo)).isFailure must be(true) - } - - "support optional embedded attribute" in { - val dbo = dbObj( - "name" -> "ze name", - "value1" -> "ze value1", - "_value2" -> 45 - ) - val test2 = MongoFormat[Test2].fromMongoValue(dbo) - test2.name mustEqual "ze name" - test2.embedded.value.value1 mustEqual "ze value1" - test2.embedded.value.value2 mustEqual 45 - - val result = MongoFormat[Test2].toMongoValue(test2) - result mustEqual dbo - } - - "ignore unknown fields" in { - val dbo = dbObj( - "name" -> "ze name", - "value1" -> "ze value1", - "_value2" -> 45, - "value4" -> true - ) - val test2 = MongoFormat[Test2].fromMongoValue(dbo) - test2.name mustEqual "ze name" - test2.embedded.value.value1 mustEqual "ze value1" - test2.embedded.value.value2 mustEqual 45 - } - - "ignore ignored fields" in { - val dbo = dbObj( - "value1" -> "ze value1", - "_value2" -> 45 - ) - val test3 = MongoFormat[Test3].fromMongoValue(dbo) - test3.name mustEqual "default" - test3.embedded.value.value1 mustEqual "ze value1" - test3.embedded.value.value2 mustEqual 45 - } - - "check for sub-fields" in { - val dbo = dbObj( - "subField" -> dbObj( - "value1" -> "ze value1", - "_value2" -> 45 - ) - ) - val test4 = MongoFormat[Test4].fromMongoValue(dbo) - test4.subField.value.embedded.value1 mustEqual "ze value1" - test4.subField.value.embedded.value2 mustEqual 45 - } - - "support the absence of optional embedded attribute" in { - val dbo = dbObj( - "name" -> "ze name" - ) - val test2 = MongoFormat[Test2].fromMongoValue(dbo) - test2.name mustEqual "ze name" - test2.embedded mustEqual None - } - - "validate the absence of some embedded attributes" in { - val dbo = dbObj( - "name" -> "ze name", - "value1" -> "ze value1" - ) - Try(MongoFormat[Test2].fromMongoValue(dbo)).isFailure must be(true) - } - } -} diff --git a/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala b/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala deleted file mode 100644 index 2edd6d21..00000000 --- a/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala +++ /dev/null @@ -1,48 +0,0 @@ -package io.sphere.mongo.generic - -import io.sphere.mongo.format.DefaultMongoFormats._ -import io.sphere.mongo.format.MongoFormat -import org.bson.BSONObject -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -import scala.jdk.CollectionConverters._ - -class MongoKeySpec extends AnyWordSpec with Matchers { - import MongoKeySpec._ - - "deriving MongoFormat" must { - "rename fields annotated with @MongoKey" in { - val test = - Test(value1 = "value1", value2 = "value2", subTest = SubTest(value2 = "other_value2")) - - val dbo = MongoFormat[Test].toMongoValue(test) - val map = dbo.asInstanceOf[BSONObject].toMap.asScala.toMap[Any, Any] - map.get("value1") must equal(Some("value1")) - map.get("value2") must equal(None) - map.get("new_value_2") must equal(Some("value2")) - map.get("new_sub_value_2") must equal(Some("other_value2")) - - val newTest = MongoFormat[Test].fromMongoValue(dbo) - newTest must be(test) - } - } -} - -object MongoKeySpec { - case class SubTest( - @MongoKey("new_sub_value_2") value2: String - ) - object SubTest { - implicit val mongo: MongoFormat[SubTest] = deriveMongoFormat - } - - case class Test( - value1: String, - @MongoKey("new_value_2") value2: String, - @MongoEmbedded subTest: SubTest - ) - object Test { - implicit val mongo: MongoFormat[Test] = deriveMongoFormat - } -} diff --git a/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala b/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala deleted file mode 100644 index ffc923eb..00000000 --- a/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala +++ /dev/null @@ -1,57 +0,0 @@ -package io.sphere.mongo.generic - -import io.sphere.mongo.MongoUtils.dbObj -import io.sphere.mongo.format.{MongoFormat, fromMongo, toMongo} -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec -import io.sphere.mongo.format.DefaultMongoFormats._ - -class MongoTypeHintFieldWithAbstractClassSpec extends AnyWordSpec with Matchers { - import MongoTypeHintFieldWithAbstractClassSpec._ - - "MongoTypeHintField (with abstract class)" must { - "allow to set another field to distinguish between types (toMongo)" in { - val user = UserWithPicture("foo-123", Medium, "http://example.com") - val expected = dbObj( - "userId" -> "foo-123", - "pictureSize" -> dbObj("pictureType" -> "Medium"), - "pictureUrl" -> "http://example.com") - - val dbo = toMongo[UserWithPicture](user) - dbo must be(expected) - } - - "allow to set another field to distinguish between types (fromMongo)" in { - val initialDbo = dbObj( - "userId" -> "foo-123", - "pictureSize" -> dbObj("pictureType" -> "Medium"), - "pictureUrl" -> "http://example.com") - - val user = fromMongo[UserWithPicture](initialDbo) - - user must be(UserWithPicture("foo-123", Medium, "http://example.com")) - - val dbo = toMongo[UserWithPicture](user) - dbo must be(initialDbo) - } - } -} - -object MongoTypeHintFieldWithAbstractClassSpec { - - @MongoTypeHintField(value = "pictureType") - sealed abstract class PictureSize - case object Small extends PictureSize - case object Medium extends PictureSize - case object Big extends PictureSize - - object PictureSize { - implicit val mongo: MongoFormat[PictureSize] = deriveMongoFormat[PictureSize] - } - - case class UserWithPicture(userId: String, pictureSize: PictureSize, pictureUrl: String) - - object UserWithPicture { - implicit val mongo: MongoFormat[UserWithPicture] = deriveMongoFormat - } -} diff --git a/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala b/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala deleted file mode 100644 index fce83a71..00000000 --- a/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala +++ /dev/null @@ -1,57 +0,0 @@ -package io.sphere.mongo.generic - -import io.sphere.mongo.MongoUtils.dbObj -import io.sphere.mongo.format.{MongoFormat, fromMongo, toMongo} -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec -import io.sphere.mongo.format.DefaultMongoFormats._ - -class MongoTypeHintFieldWithSealedTraitSpec extends AnyWordSpec with Matchers { - import MongoTypeHintFieldWithSealedTraitSpec._ - - "MongoTypeHintField (with sealed trait)" must { - "allow to set another field to distinguish between types (toMongo)" in { - val user = UserWithPicture("foo-123", Medium, "http://example.com") - val expected = dbObj( - "userId" -> "foo-123", - "pictureSize" -> dbObj("pictureType" -> "Medium"), - "pictureUrl" -> "http://example.com") - - val dbo = toMongo[UserWithPicture](user) - dbo must be(expected) - } - - "allow to set another field to distinguish between types (fromMongo)" in { - val initialDbo = dbObj( - "userId" -> "foo-123", - "pictureSize" -> dbObj("pictureType" -> "Medium"), - "pictureUrl" -> "http://example.com") - - val user = fromMongo[UserWithPicture](initialDbo) - - user must be(UserWithPicture("foo-123", Medium, "http://example.com")) - - val dbo = toMongo[UserWithPicture](user) - dbo must be(initialDbo) - } - } -} - -object MongoTypeHintFieldWithSealedTraitSpec { - - @MongoTypeHintField(value = "pictureType") - sealed trait PictureSize - case object Small extends PictureSize - case object Medium extends PictureSize - case object Big extends PictureSize - - object PictureSize { - implicit val mongo: MongoFormat[PictureSize] = deriveMongoFormat[PictureSize] - } - - case class UserWithPicture(userId: String, pictureSize: PictureSize, pictureUrl: String) - - object UserWithPicture { - implicit val mongo: MongoFormat[UserWithPicture] = deriveMongoFormat - } -} diff --git a/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala b/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala deleted file mode 100644 index 5e909a56..00000000 --- a/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala +++ /dev/null @@ -1,172 +0,0 @@ -package io.sphere.mongo.generic - -import com.mongodb.DBObject -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec -import io.sphere.mongo.MongoUtils.dbObj -import io.sphere.mongo.format.DefaultMongoFormats._ -import io.sphere.mongo.format.MongoFormat -import org.scalatest.Assertion - -class SumTypesDerivingSpec extends AnyWordSpec with Matchers { - import SumTypesDerivingSpec._ - - "Serializing sum types" must { - "use 'type' as default field" in { - check(Color1.format, Color1.Red, dbObj("type" -> "Red")) - - check(Color1.format, Color1.Custom("2356"), dbObj("type" -> "Custom", "rgb" -> "2356")) - } - - "use custom field" in { - check(Color2.format, Color2.Red, dbObj("color" -> "Red")) - - check(Color2.format, Color2.Custom("2356"), dbObj("color" -> "Custom", "rgb" -> "2356")) - } - - "use custom values" in { - check(Color3.format, Color3.Red, dbObj("type" -> "red")) - - check(Color3.format, Color3.Custom("2356"), dbObj("type" -> "custom", "rgb" -> "2356")) - } - - "use custom field & values" in pendingUntilFixed { - check(Color4.format, Color4.Red, dbObj("color" -> "red")) - - check(Color4.format, Color4.Custom("2356"), dbObj("color" -> "custom", "rgb" -> "2356")) - } - - "not allow specifying different custom field" in pendingUntilFixed { - // to serialize Custom, should we use type "color" or "color-custom"? - "deriveMongoFormat[Color5]" mustNot compile - } - - "not allow specifying different custom field on intermediate level" in { - // to serialize Custom, should we use type "color" or "color-custom"? - "deriveMongoFormat[Color6]" mustNot compile - } - - "use intermediate level" in { - deriveMongoFormat[Color7] - } - - "do not use sealed trait info when using a case class directly" in { - check(Color8.format, Color8.Custom("2356"), dbObj("type" -> "Custom", "rgb" -> "2356")) - - check(Color8.Custom.format, Color8.Custom("2356"), dbObj("rgb" -> "2356")) - - // unless annotated - - check( - Color8.format, - Color8.CustomAnnotated("2356"), - dbObj("type" -> "CustomAnnotated", "rgb" -> "2356")) - - check( - Color8.CustomAnnotated.format, - Color8.CustomAnnotated("2356"), - dbObj("type" -> "CustomAnnotated", "rgb" -> "2356")) - } - - "use default values if custom values are empty" in { - check(Color9.format, Color9.Red, dbObj("type" -> "Red")) - - check(Color9.format, Color9.Custom("2356"), dbObj("type" -> "Custom", "rgb" -> "2356")) - } - } - -} - -object SumTypesDerivingSpec { - import Matchers._ - - def check[A, B <: A](format: MongoFormat[A], b: B, dbo: DBObject): Assertion = { - val serialized = format.toMongoValue(b) - serialized must be(dbo) - - format.fromMongoValue(serialized) must be(b) - } - - sealed trait Color1 - object Color1 { - case object Red extends Color1 - case class Custom(rgb: String) extends Color1 - val format = deriveMongoFormat[Color1] - } - - @MongoTypeHintField("color") - sealed trait Color2 - object Color2 { - case object Red extends Color2 - case class Custom(rgb: String) extends Color2 - val format = deriveMongoFormat[Color2] - } - - sealed trait Color3 - object Color3 { - @MongoTypeHint("red") case object Red extends Color3 - @MongoTypeHint("custom") case class Custom(rgb: String) extends Color3 - val format = deriveMongoFormat[Color3] - } - - @MongoTypeHintField("color") - sealed trait Color4 - object Color4 { - @MongoTypeHint("red") case object Red extends Color4 - @MongoTypeHint("custom") case class Custom(rgb: String) extends Color4 - val format = deriveMongoFormat[Color4] - } - - @MongoTypeHintField("color") - sealed trait Color5 - object Color5 { - @MongoTypeHint("red") - case object Red extends Color5 - @MongoTypeHintField("color-custom") - @MongoTypeHint("custom") - case class Custom(rgb: String) extends Color5 - } - - @MongoTypeHintField("color") - sealed trait Color6 - object Color6 { - @MongoTypeHintField("color-custom") - abstract class MyColor extends Color6 - @MongoTypeHint("red") - case object Red extends MyColor - @MongoTypeHint("custom") - case class Custom(rgb: String) extends MyColor - } - - sealed trait Color7 - sealed trait Color7a extends Color7 - object Color7 { - case object Red extends Color7a - case class Custom(rgb: String) extends Color7a - } - - sealed trait Color8 - object Color8 { - case object Red extends Color8 - case class Custom(rgb: String) extends Color8 - object Custom { - val format = deriveMongoFormat[Custom] - } - @MongoTypeHintField("type") - case class CustomAnnotated(rgb: String) extends Color8 - object CustomAnnotated { - val format = deriveMongoFormat[CustomAnnotated] - } - val format = deriveMongoFormat[Color8] - } - - sealed trait Color9 - object Color9 { - @MongoTypeHint("") - case object Red extends Color9 - @MongoTypeHint(" ") - case class Custom(rgb: String) extends Color9 - val format = deriveMongoFormat[Color9] - } - -} From cb2af673a2364cf8a90b7eb9b6e66426034247e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Brunk?= Date: Fri, 2 May 2025 14:53:51 +0200 Subject: [PATCH 104/142] Cross-compile all submodules; disable fmpp on Scala 3 --- .../json/DateTimeFromJSONBenchmark.scala | 0 .../json/EnumFromJSONBenchmark.scala | 0 .../json/FromJsonBenchmark.scala | 0 .../json/FromMongoBenchmark.scala | 0 .../json/JsonBenchmark.scala | 0 .../json/ParseJsonBenchmark.scala | 0 .../json/ToJsonBenchmark.scala | 0 .../json/ToMongoValueBenchmark.scala | 0 build.sbt | 20 ++++++++----------- .../sphere/json/DeriveSingletonJSONSpec.scala | 0 .../io/sphere/json/ForProductNSpec.scala | 0 .../io/sphere/json/JSONEmbeddedSpec.scala | 0 .../io/sphere/json/JSONSpec.scala | 0 .../io/sphere/json/NullHandlingSpec.scala | 0 .../io/sphere/json/OptionReaderSpec.scala | 0 .../io/sphere/json/TypesSwitchSpec.scala | 0 .../json/generic/DefaultValuesSpec.scala | 0 .../io/sphere/json/generic/JSONKeySpec.scala | 0 .../json/generic/JsonTypeHintFieldSpec.scala | 0 .../scala/io/sphere/mongo/MongoUtils.scala | 9 --------- .../mongo/generic/MongoFormatMacros.scala | 0 .../sphere/mongo/generic/package.fmpp.scala | 0 project/Fmpp.scala | 3 +-- 23 files changed, 9 insertions(+), 23 deletions(-) rename benchmarks/src/main/{scala => scala-2}/json/DateTimeFromJSONBenchmark.scala (100%) rename benchmarks/src/main/{scala => scala-2}/json/EnumFromJSONBenchmark.scala (100%) rename benchmarks/src/main/{scala => scala-2}/json/FromJsonBenchmark.scala (100%) rename benchmarks/src/main/{scala => scala-2}/json/FromMongoBenchmark.scala (100%) rename benchmarks/src/main/{scala => scala-2}/json/JsonBenchmark.scala (100%) rename benchmarks/src/main/{scala => scala-2}/json/ParseJsonBenchmark.scala (100%) rename benchmarks/src/main/{scala => scala-2}/json/ToJsonBenchmark.scala (100%) rename benchmarks/src/main/{scala => scala-2}/json/ToMongoValueBenchmark.scala (100%) rename json/json-derivation/src/test/{scala => scala-2}/io/sphere/json/DeriveSingletonJSONSpec.scala (100%) rename json/json-derivation/src/test/{scala => scala-2}/io/sphere/json/ForProductNSpec.scala (100%) rename json/json-derivation/src/test/{scala => scala-2}/io/sphere/json/JSONEmbeddedSpec.scala (100%) rename json/json-derivation/src/test/{scala => scala-2}/io/sphere/json/JSONSpec.scala (100%) rename json/json-derivation/src/test/{scala => scala-2}/io/sphere/json/NullHandlingSpec.scala (100%) rename json/json-derivation/src/test/{scala => scala-2}/io/sphere/json/OptionReaderSpec.scala (100%) rename json/json-derivation/src/test/{scala => scala-2}/io/sphere/json/TypesSwitchSpec.scala (100%) rename json/json-derivation/src/test/{scala => scala-2}/io/sphere/json/generic/DefaultValuesSpec.scala (100%) rename json/json-derivation/src/test/{scala => scala-2}/io/sphere/json/generic/JSONKeySpec.scala (100%) rename json/json-derivation/src/test/{scala => scala-2}/io/sphere/json/generic/JsonTypeHintFieldSpec.scala (100%) delete mode 100644 mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/MongoUtils.scala rename mongo/mongo-derivation/src/main/{scala => scala-2}/io/sphere/mongo/generic/MongoFormatMacros.scala (100%) rename mongo/mongo-derivation/src/main/{scala => scala-2}/io/sphere/mongo/generic/package.fmpp.scala (100%) diff --git a/benchmarks/src/main/scala/json/DateTimeFromJSONBenchmark.scala b/benchmarks/src/main/scala-2/json/DateTimeFromJSONBenchmark.scala similarity index 100% rename from benchmarks/src/main/scala/json/DateTimeFromJSONBenchmark.scala rename to benchmarks/src/main/scala-2/json/DateTimeFromJSONBenchmark.scala diff --git a/benchmarks/src/main/scala/json/EnumFromJSONBenchmark.scala b/benchmarks/src/main/scala-2/json/EnumFromJSONBenchmark.scala similarity index 100% rename from benchmarks/src/main/scala/json/EnumFromJSONBenchmark.scala rename to benchmarks/src/main/scala-2/json/EnumFromJSONBenchmark.scala diff --git a/benchmarks/src/main/scala/json/FromJsonBenchmark.scala b/benchmarks/src/main/scala-2/json/FromJsonBenchmark.scala similarity index 100% rename from benchmarks/src/main/scala/json/FromJsonBenchmark.scala rename to benchmarks/src/main/scala-2/json/FromJsonBenchmark.scala diff --git a/benchmarks/src/main/scala/json/FromMongoBenchmark.scala b/benchmarks/src/main/scala-2/json/FromMongoBenchmark.scala similarity index 100% rename from benchmarks/src/main/scala/json/FromMongoBenchmark.scala rename to benchmarks/src/main/scala-2/json/FromMongoBenchmark.scala diff --git a/benchmarks/src/main/scala/json/JsonBenchmark.scala b/benchmarks/src/main/scala-2/json/JsonBenchmark.scala similarity index 100% rename from benchmarks/src/main/scala/json/JsonBenchmark.scala rename to benchmarks/src/main/scala-2/json/JsonBenchmark.scala diff --git a/benchmarks/src/main/scala/json/ParseJsonBenchmark.scala b/benchmarks/src/main/scala-2/json/ParseJsonBenchmark.scala similarity index 100% rename from benchmarks/src/main/scala/json/ParseJsonBenchmark.scala rename to benchmarks/src/main/scala-2/json/ParseJsonBenchmark.scala diff --git a/benchmarks/src/main/scala/json/ToJsonBenchmark.scala b/benchmarks/src/main/scala-2/json/ToJsonBenchmark.scala similarity index 100% rename from benchmarks/src/main/scala/json/ToJsonBenchmark.scala rename to benchmarks/src/main/scala-2/json/ToJsonBenchmark.scala diff --git a/benchmarks/src/main/scala/json/ToMongoValueBenchmark.scala b/benchmarks/src/main/scala-2/json/ToMongoValueBenchmark.scala similarity index 100% rename from benchmarks/src/main/scala/json/ToMongoValueBenchmark.scala rename to benchmarks/src/main/scala-2/json/ToMongoValueBenchmark.scala diff --git a/build.sbt b/build.sbt index 1ca34117..dba797cf 100644 --- a/build.sbt +++ b/build.sbt @@ -114,22 +114,16 @@ lazy val `sphere-libs` = project lazy val `sphere-util` = project .in(file("./util")) .settings(standardSettings: _*) - .settings(scalaVersion := scala3) - .settings(crossScalaVersions := Seq(scala213, scala3)) .settings(homepage := Some(url("https://github.com/commercetools/sphere-scala-libs/README.md"))) lazy val `sphere-json-core` = project .in(file("./json/json-core")) .settings(standardSettings: _*) - .settings(scalaVersion := scala3) - .settings(crossScalaVersions := Seq(scala213, scala3)) .dependsOn(`sphere-util`) lazy val `sphere-mongo-core` = project .in(file("./mongo/mongo-core")) .settings(standardSettings: _*) - .settings(scalaVersion := scala3) - .settings(crossScalaVersions := Seq(scala213, scala3)) .dependsOn(`sphere-util`) // Scala 2 modules @@ -138,7 +132,10 @@ lazy val `sphere-json-derivation` = project .in(file("./json/json-derivation")) .settings(standardSettings: _*) .settings(Fmpp.settings: _*) - .settings(crossScalaVersions := Seq(scala213)) + .settings( + inConfig(Compile)( + sourceGenerators ++= (if (scalaVersion.value.startsWith("2")) Seq(Fmpp.fmpp.taskValue) + else Seq()))) .dependsOn(`sphere-json-core`) lazy val `sphere-json` = project @@ -146,23 +143,23 @@ lazy val `sphere-json` = project .settings(standardSettings: _*) .settings(homepage := Some( url("https://github.com/commercetools/sphere-scala-libs/blob/master/json/README.md"))) - .settings(crossScalaVersions := Seq(scala213)) .dependsOn(`sphere-json-core`, `sphere-json-derivation`) lazy val `sphere-mongo-derivation` = project .in(file("./mongo/mongo-derivation")) .settings(standardSettings: _*) .settings(Fmpp.settings: _*) - .settings(crossScalaVersions := Seq(scala213)) + .settings( + inConfig(Compile)( + sourceGenerators ++= (if (scalaVersion.value.startsWith("2")) Seq(Fmpp.fmpp.taskValue) + else Seq()))) .dependsOn(`sphere-mongo-core`) - lazy val `sphere-mongo` = project .in(file("./mongo")) .settings(standardSettings: _*) .settings(homepage := Some( url("https://github.com/commercetools/sphere-scala-libs/blob/master/mongo/README.md"))) - .settings(crossScalaVersions := Seq(scala213)) .dependsOn(`sphere-mongo-core`, `sphere-mongo-derivation`) // benchmarks @@ -170,6 +167,5 @@ lazy val `sphere-mongo` = project lazy val benchmarks = project .settings(standardSettings: _*) .settings(publishArtifact := false, publish := {}) - .settings(crossScalaVersions := Seq(scala213)) .enablePlugins(JmhPlugin) .dependsOn(`sphere-util`, `sphere-json`, `sphere-mongo`) diff --git a/json/json-derivation/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala b/json/json-derivation/src/test/scala-2/io/sphere/json/DeriveSingletonJSONSpec.scala similarity index 100% rename from json/json-derivation/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala rename to json/json-derivation/src/test/scala-2/io/sphere/json/DeriveSingletonJSONSpec.scala diff --git a/json/json-derivation/src/test/scala/io/sphere/json/ForProductNSpec.scala b/json/json-derivation/src/test/scala-2/io/sphere/json/ForProductNSpec.scala similarity index 100% rename from json/json-derivation/src/test/scala/io/sphere/json/ForProductNSpec.scala rename to json/json-derivation/src/test/scala-2/io/sphere/json/ForProductNSpec.scala diff --git a/json/json-derivation/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala b/json/json-derivation/src/test/scala-2/io/sphere/json/JSONEmbeddedSpec.scala similarity index 100% rename from json/json-derivation/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala rename to json/json-derivation/src/test/scala-2/io/sphere/json/JSONEmbeddedSpec.scala diff --git a/json/json-derivation/src/test/scala/io/sphere/json/JSONSpec.scala b/json/json-derivation/src/test/scala-2/io/sphere/json/JSONSpec.scala similarity index 100% rename from json/json-derivation/src/test/scala/io/sphere/json/JSONSpec.scala rename to json/json-derivation/src/test/scala-2/io/sphere/json/JSONSpec.scala diff --git a/json/json-derivation/src/test/scala/io/sphere/json/NullHandlingSpec.scala b/json/json-derivation/src/test/scala-2/io/sphere/json/NullHandlingSpec.scala similarity index 100% rename from json/json-derivation/src/test/scala/io/sphere/json/NullHandlingSpec.scala rename to json/json-derivation/src/test/scala-2/io/sphere/json/NullHandlingSpec.scala diff --git a/json/json-derivation/src/test/scala/io/sphere/json/OptionReaderSpec.scala b/json/json-derivation/src/test/scala-2/io/sphere/json/OptionReaderSpec.scala similarity index 100% rename from json/json-derivation/src/test/scala/io/sphere/json/OptionReaderSpec.scala rename to json/json-derivation/src/test/scala-2/io/sphere/json/OptionReaderSpec.scala diff --git a/json/json-derivation/src/test/scala/io/sphere/json/TypesSwitchSpec.scala b/json/json-derivation/src/test/scala-2/io/sphere/json/TypesSwitchSpec.scala similarity index 100% rename from json/json-derivation/src/test/scala/io/sphere/json/TypesSwitchSpec.scala rename to json/json-derivation/src/test/scala-2/io/sphere/json/TypesSwitchSpec.scala diff --git a/json/json-derivation/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala b/json/json-derivation/src/test/scala-2/io/sphere/json/generic/DefaultValuesSpec.scala similarity index 100% rename from json/json-derivation/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala rename to json/json-derivation/src/test/scala-2/io/sphere/json/generic/DefaultValuesSpec.scala diff --git a/json/json-derivation/src/test/scala/io/sphere/json/generic/JSONKeySpec.scala b/json/json-derivation/src/test/scala-2/io/sphere/json/generic/JSONKeySpec.scala similarity index 100% rename from json/json-derivation/src/test/scala/io/sphere/json/generic/JSONKeySpec.scala rename to json/json-derivation/src/test/scala-2/io/sphere/json/generic/JSONKeySpec.scala diff --git a/json/json-derivation/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala b/json/json-derivation/src/test/scala-2/io/sphere/json/generic/JsonTypeHintFieldSpec.scala similarity index 100% rename from json/json-derivation/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala rename to json/json-derivation/src/test/scala-2/io/sphere/json/generic/JsonTypeHintFieldSpec.scala diff --git a/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/MongoUtils.scala b/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/MongoUtils.scala deleted file mode 100644 index 026b53dc..00000000 --- a/mongo/mongo-derivation-magnolia/src/test/scala/io/sphere/mongo/MongoUtils.scala +++ /dev/null @@ -1,9 +0,0 @@ -package io.sphere.mongo -import com.mongodb.BasicDBObject - -object MongoUtils { - - def dbObj(pairs: (String, Any)*) = - pairs.foldLeft(new BasicDBObject) { case (obj, (key, value)) => obj.append(key, value) } - -} diff --git a/mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/MongoFormatMacros.scala b/mongo/mongo-derivation/src/main/scala-2/io/sphere/mongo/generic/MongoFormatMacros.scala similarity index 100% rename from mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/MongoFormatMacros.scala rename to mongo/mongo-derivation/src/main/scala-2/io/sphere/mongo/generic/MongoFormatMacros.scala diff --git a/mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/package.fmpp.scala b/mongo/mongo-derivation/src/main/scala-2/io/sphere/mongo/generic/package.fmpp.scala similarity index 100% rename from mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/package.fmpp.scala rename to mongo/mongo-derivation/src/main/scala-2/io/sphere/mongo/generic/package.fmpp.scala diff --git a/project/Fmpp.scala b/project/Fmpp.scala index 31b41844..6bdc8197 100644 --- a/project/Fmpp.scala +++ b/project/Fmpp.scala @@ -9,7 +9,7 @@ import java.io.File // inspiration: https://github.com/sbt/sbt-fmpp/blob/master/src/main/scala/FmppPlugin.scala object Fmpp { - private lazy val fmpp = TaskKey[Seq[File]]("fmpp") + lazy val fmpp = TaskKey[Seq[File]]("fmpp") private lazy val fmppOptions = SettingKey[Seq[String]]("fmppOptions") private lazy val FmppConfig = config("fmpp") hide @@ -23,7 +23,6 @@ object Fmpp { private def fmppConfigSettings(c: Configuration): Seq[Setting[_]] = inConfig(c)( Seq( - Compile / sourceGenerators += fmpp.taskValue, fmpp := fmppTask.value, sources := managedSources.value )) From 9e9cbc5fa433e005038baa1230ee57bceb4e4460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Brunk?= Date: Fri, 2 May 2025 14:56:27 +0200 Subject: [PATCH 105/142] Cross-compile all submodules; disable fmpp on Scala 3 --- .../io/sphere/mongo/generic/MongoFormatMacros.scala | 0 .../{scala-2 => scala}/io/sphere/mongo/generic/package.fmpp.scala | 0 .../src/test/{scala => scala-2}/io/sphere/mongo/MongoUtils.scala | 0 .../{scala => scala-2}/io/sphere/mongo/SerializationTest.scala | 0 .../io/sphere/mongo/format/OptionMongoFormatSpec.scala | 0 .../io/sphere/mongo/generic/DefaultValuesSpec.scala | 0 .../io/sphere/mongo/generic/DeriveMongoformatSpec.scala | 0 .../io/sphere/mongo/generic/MongoEmbeddedSpec.scala | 0 .../{scala => scala-2}/io/sphere/mongo/generic/MongoKeySpec.scala | 0 .../mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala | 0 .../mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala | 0 .../io/sphere/mongo/generic/SumTypesDerivingSpec.scala | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename mongo/mongo-derivation/src/main/{scala-2 => scala}/io/sphere/mongo/generic/MongoFormatMacros.scala (100%) rename mongo/mongo-derivation/src/main/{scala-2 => scala}/io/sphere/mongo/generic/package.fmpp.scala (100%) rename mongo/mongo-derivation/src/test/{scala => scala-2}/io/sphere/mongo/MongoUtils.scala (100%) rename mongo/mongo-derivation/src/test/{scala => scala-2}/io/sphere/mongo/SerializationTest.scala (100%) rename mongo/mongo-derivation/src/test/{scala => scala-2}/io/sphere/mongo/format/OptionMongoFormatSpec.scala (100%) rename mongo/mongo-derivation/src/test/{scala => scala-2}/io/sphere/mongo/generic/DefaultValuesSpec.scala (100%) rename mongo/mongo-derivation/src/test/{scala => scala-2}/io/sphere/mongo/generic/DeriveMongoformatSpec.scala (100%) rename mongo/mongo-derivation/src/test/{scala => scala-2}/io/sphere/mongo/generic/MongoEmbeddedSpec.scala (100%) rename mongo/mongo-derivation/src/test/{scala => scala-2}/io/sphere/mongo/generic/MongoKeySpec.scala (100%) rename mongo/mongo-derivation/src/test/{scala => scala-2}/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala (100%) rename mongo/mongo-derivation/src/test/{scala => scala-2}/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala (100%) rename mongo/mongo-derivation/src/test/{scala => scala-2}/io/sphere/mongo/generic/SumTypesDerivingSpec.scala (100%) diff --git a/mongo/mongo-derivation/src/main/scala-2/io/sphere/mongo/generic/MongoFormatMacros.scala b/mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/MongoFormatMacros.scala similarity index 100% rename from mongo/mongo-derivation/src/main/scala-2/io/sphere/mongo/generic/MongoFormatMacros.scala rename to mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/MongoFormatMacros.scala diff --git a/mongo/mongo-derivation/src/main/scala-2/io/sphere/mongo/generic/package.fmpp.scala b/mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/package.fmpp.scala similarity index 100% rename from mongo/mongo-derivation/src/main/scala-2/io/sphere/mongo/generic/package.fmpp.scala rename to mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/package.fmpp.scala diff --git a/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/MongoUtils.scala b/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/MongoUtils.scala similarity index 100% rename from mongo/mongo-derivation/src/test/scala/io/sphere/mongo/MongoUtils.scala rename to mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/MongoUtils.scala diff --git a/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/SerializationTest.scala b/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/SerializationTest.scala similarity index 100% rename from mongo/mongo-derivation/src/test/scala/io/sphere/mongo/SerializationTest.scala rename to mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/SerializationTest.scala diff --git a/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala b/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/format/OptionMongoFormatSpec.scala similarity index 100% rename from mongo/mongo-derivation/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala rename to mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/format/OptionMongoFormatSpec.scala diff --git a/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala b/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/DefaultValuesSpec.scala similarity index 100% rename from mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala rename to mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/DefaultValuesSpec.scala diff --git a/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/DeriveMongoformatSpec.scala b/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/DeriveMongoformatSpec.scala similarity index 100% rename from mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/DeriveMongoformatSpec.scala rename to mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/DeriveMongoformatSpec.scala diff --git a/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala b/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/MongoEmbeddedSpec.scala similarity index 100% rename from mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala rename to mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/MongoEmbeddedSpec.scala diff --git a/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala b/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/MongoKeySpec.scala similarity index 100% rename from mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala rename to mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/MongoKeySpec.scala diff --git a/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala b/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala similarity index 100% rename from mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala rename to mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala diff --git a/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala b/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala similarity index 100% rename from mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala rename to mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala diff --git a/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala b/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/SumTypesDerivingSpec.scala similarity index 100% rename from mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/SumTypesDerivingSpec.scala rename to mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/SumTypesDerivingSpec.scala From 7227566393151c41c4149b4c1022b1405e9a9735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Brunk?= Date: Fri, 2 May 2025 14:37:55 +0200 Subject: [PATCH 106/142] Simplify CI cross-compilation --- .github/workflows/ci.yml | 11 +++-------- build.sbt | 33 --------------------------------- 2 files changed, 3 insertions(+), 41 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6f0de1f..d101af06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,16 +50,11 @@ jobs: - name: Check that workflows are up to date run: sbt '++ ${{ matrix.scala }}' githubWorkflowCheck - - name: Build Scala 2 project - if: matrix.scala != '3.3.5' - run: sbt '++ ${{ matrix.scala }}' sphere-util/test sphere-json/test sphere-json-core/test sphere-json-derivation/test sphere-mongo/test sphere-mongo-core/test sphere-mongo-derivation/test sphere-mongo-derivation-magnolia/test benchmarks/test - - - name: Build Scala 3 project - if: matrix.scala == '3.3.5' - run: sbt '++ ${{ matrix.scala }}' sphere-util/test sphere-mongo-core/test sphere-json-core/test + - name: Build project + run: sbt '++ ${{ matrix.scala }}' test - name: Compress target directories - run: tar cf targets.tar benchmarks/target mongo/target json/json-core/target mongo/mongo-core/target json/target util/target json/json-derivation/target mongo/mongo-derivation-magnolia/target target mongo/mongo-derivation/target project/target + run: tar cf targets.tar benchmarks/target mongo/target json/json-core/target mongo/mongo-core/target json/target util/target json/json-derivation/target target mongo/mongo-derivation/target project/target - name: Upload target directories uses: actions/upload-artifact@v4 diff --git a/build.sbt b/build.sbt index dba797cf..a3bdbc59 100644 --- a/build.sbt +++ b/build.sbt @@ -13,35 +13,6 @@ ThisBuild / githubWorkflowBuildPreamble ++= List( ) ThisBuild / githubWorkflowBuildMatrixFailFast := Some(false) -// workaround for CI because `sbt ++3.3.4 test` used by sbt-github-actions -// still tries to compile the Scala 2 only projects leading to weird issues -// note that `sbt +test` is working fine to run cross-compiled tests locally -ThisBuild / githubWorkflowBuild := Seq( - WorkflowStep.Sbt( - commands = List( - "sphere-util/test", - "sphere-json/test", - "sphere-json-core/test", - "sphere-json-derivation/test", - "sphere-mongo/test", - "sphere-mongo-core/test", - "sphere-mongo-derivation/test", - "benchmarks/test" - ), - name = Some("Build Scala 2 project"), - cond = Some(s"matrix.scala != '$scala3'") - ), - WorkflowStep.Sbt( - commands = List( - "sphere-util/test", - "sphere-mongo-core/test", - "sphere-json-core/test" - ), - name = Some("Build Scala 3 project"), - cond = Some(s"matrix.scala == '$scala3'") - ) -) - // Release inThisBuild( @@ -109,8 +80,6 @@ lazy val `sphere-libs` = project `benchmarks` ) -// Scala 2 & 3 modules - lazy val `sphere-util` = project .in(file("./util")) .settings(standardSettings: _*) @@ -126,8 +95,6 @@ lazy val `sphere-mongo-core` = project .settings(standardSettings: _*) .dependsOn(`sphere-util`) -// Scala 2 modules - lazy val `sphere-json-derivation` = project .in(file("./json/json-derivation")) .settings(standardSettings: _*) From a2d6c2d92f267c0584e442ac2e45c377e8ea16fc Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 8 May 2025 10:23:56 +0200 Subject: [PATCH 107/142] moving more json derivation tests to the common section --- build.sbt | 2 +- .../io/sphere/json/OptionReaderSpec.scala | 150 ------------------ .../json/generic/JsonTypeHintFieldSpec.scala | 61 ------- .../io/sphere/json/JSONEmbeddedSpec.scala | 131 --------------- .../io/sphere/json/NullHandlingSpec.scala | 68 -------- .../json/generic/DefaultValuesSpec.scala | 33 ---- .../io/sphere/json/generic/JSONKeySpec.scala | 45 ------ .../sphere/json/DeriveSingletonJSONSpec.scala | 1 + .../io/sphere/json/generic/JSONSpec.scala | 0 .../json/generic/JsonTypeSwitchSpec.scala | 0 .../io/sphere/json/JSONEmbeddedSpec.scala | 12 +- .../io/sphere/json/NullHandlingSpec.scala | 2 +- .../io/sphere/json/OptionReaderSpec.scala | 8 +- .../json/generic/DefaultValuesSpec.scala | 6 +- .../io/sphere/json/generic/JSONKeySpec.scala | 5 +- .../json/generic/JsonTypeHintFieldSpec.scala | 13 +- 16 files changed, 27 insertions(+), 510 deletions(-) delete mode 100644 json/json-core/src/test/scala-3/io/sphere/json/OptionReaderSpec.scala delete mode 100644 json/json-core/src/test/scala-3/io/sphere/json/generic/JsonTypeHintFieldSpec.scala delete mode 100644 json/json-derivation/src/test/scala-2/io/sphere/json/JSONEmbeddedSpec.scala delete mode 100644 json/json-derivation/src/test/scala-2/io/sphere/json/NullHandlingSpec.scala delete mode 100644 json/json-derivation/src/test/scala-2/io/sphere/json/generic/DefaultValuesSpec.scala delete mode 100644 json/json-derivation/src/test/scala-2/io/sphere/json/generic/JSONKeySpec.scala rename json/{json-core => json-derivation}/src/test/scala-3/io/sphere/json/DeriveSingletonJSONSpec.scala (99%) rename json/{json-core => json-derivation}/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala (100%) rename json/{json-core => json-derivation}/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala (100%) rename json/{json-core/src/test/scala-3 => json-derivation/src/test/scala}/io/sphere/json/JSONEmbeddedSpec.scala (92%) rename json/{json-core/src/test/scala-3 => json-derivation/src/test/scala}/io/sphere/json/NullHandlingSpec.scala (97%) rename json/json-derivation/src/test/{scala-2 => scala}/io/sphere/json/OptionReaderSpec.scala (94%) rename json/{json-core/src/test/scala-3 => json-derivation/src/test/scala}/io/sphere/json/generic/DefaultValuesSpec.scala (89%) rename json/{json-core/src/test/scala-3 => json-derivation/src/test/scala}/io/sphere/json/generic/JSONKeySpec.scala (91%) rename json/json-derivation/src/test/{scala-2 => scala}/io/sphere/json/generic/JsonTypeHintFieldSpec.scala (82%) diff --git a/build.sbt b/build.sbt index a3bdbc59..f55dea4d 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ lazy val scala3 = "3.3.5" // sbt-github-actions needs configuration in `ThisBuild` ThisBuild / crossScalaVersions := Seq(scala213, scala3) -ThisBuild / scalaVersion := scala213 +ThisBuild / scalaVersion := scala3 ThisBuild / githubWorkflowPublishTargetBranches := List() ThisBuild / githubWorkflowJavaVersions := List(JavaSpec.temurin("21")) ThisBuild / githubWorkflowBuildPreamble ++= List( diff --git a/json/json-core/src/test/scala-3/io/sphere/json/OptionReaderSpec.scala b/json/json-core/src/test/scala-3/io/sphere/json/OptionReaderSpec.scala deleted file mode 100644 index 8461d962..00000000 --- a/json/json-core/src/test/scala-3/io/sphere/json/OptionReaderSpec.scala +++ /dev/null @@ -1,150 +0,0 @@ -package io.sphere.json - -import io.sphere.json.generic._ -import org.json4s.{JArray, JLong, JNothing, JObject, JString} -import org.scalatest.OptionValues -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec -import org.json4s.DefaultJsonFormats.given - -object OptionReaderSpec { - - case class SimpleClass(value1: String, value2: Int) - - object SimpleClass { - given JSON[SimpleClass] = deriveJSON[SimpleClass] - } - - case class ComplexClass(name: String, simpleClass: Option[SimpleClass]) - - object ComplexClass { - given JSON[ComplexClass] = deriveJSON[ComplexClass] - } - - case class MapClass(id: Long, map: Option[Map[String, String]]) - object MapClass { - given JSON[MapClass] = deriveJSON[MapClass] - } - - case class ListClass(id: Long, list: Option[List[String]]) - object ListClass { - given JSON[ListClass] = deriveJSON[ListClass] - } -} - -class OptionReaderSpec extends AnyWordSpec with Matchers with OptionValues { - import OptionReaderSpec._ - - "OptionReader" should { - "handle presence of all fields" in { - val json = - """{ - | "value1": "a", - | "value2": 45 - |} - """.stripMargin - val result = getFromJSON[Option[SimpleClass]](json) - result.value.value1 mustEqual "a" - result.value.value2 mustEqual 45 - } - - "handle presence of all fields mixed with ignored fields" in { - val json = - """{ - | "value1": "a", - | "value2": 45, - | "value3": "b" - |} - """.stripMargin - val result = getFromJSON[Option[SimpleClass]](json) - result.value.value1 mustEqual "a" - result.value.value2 mustEqual 45 - } - - "handle presence of not all the fields" in { - val json = """{ "value1": "a" }""" - fromJSON[Option[SimpleClass]](json).isInvalid must be(true) - } - - "handle absence of all fields" in { - val json = "{}" - val result = getFromJSON[Option[SimpleClass]](json) - result must be(None) - } - - "handle optional map" in { - getFromJValue[MapClass](JObject("id" -> JLong(1L))) mustEqual MapClass(1L, None) - - getFromJValue[MapClass](JObject("id" -> JLong(1L), "map" -> JObject())) mustEqual - MapClass(1L, Some(Map.empty)) - - getFromJValue[MapClass]( - JObject("id" -> JLong(1L), "map" -> JObject("a" -> JString("b")))) mustEqual - MapClass(1L, Some(Map("a" -> "b"))) - - toJValue[MapClass](MapClass(1L, None)) mustEqual - JObject("id" -> JLong(1L), "map" -> JNothing) - toJValue[MapClass](MapClass(1L, Some(Map()))) mustEqual - JObject("id" -> JLong(1L), "map" -> JObject()) - toJValue[MapClass](MapClass(1L, Some(Map("a" -> "b")))) mustEqual - JObject("id" -> JLong(1L), "map" -> JObject("a" -> JString("b"))) - } - - "handle optional list" in { - getFromJValue[ListClass]( - JObject("id" -> JLong(1L), "list" -> JArray(List(JString("hi"))))) mustEqual - ListClass(1L, Some(List("hi"))) - getFromJValue[ListClass](JObject("id" -> JLong(1L), "list" -> JArray(List.empty))) mustEqual - ListClass(1L, Some(List())) - getFromJValue[ListClass](JObject("id" -> JLong(1L))) mustEqual - ListClass(1L, None) - - toJValue(ListClass(1L, Some(List("hi")))) mustEqual JObject( - "id" -> JLong(1L), - "list" -> JArray(List(JString("hi")))) - toJValue(ListClass(1L, Some(List.empty))) mustEqual JObject( - "id" -> JLong(1L), - "list" -> JArray(List.empty)) - toJValue(ListClass(1L, None)) mustEqual JObject("id" -> JLong(1L), "list" -> JNothing) - } - - "handle absence of all fields mixed with ignored fields" in { - val json = """{ "value3": "a" }""" - val result = getFromJSON[Option[SimpleClass]](json) - result must be(None) - } - - "consider all fields if the data type does not impose any restriction" in { - val json = - """{ - | "key1": "value1", - | "key2": "value2" - |} - """.stripMargin - val expected = Map("key1" -> "value1", "key2" -> "value2") - val result = getFromJSON[Map[String, String]](json) - result mustEqual expected - - val maybeResult = getFromJSON[Option[Map[String, String]]](json) - maybeResult.value mustEqual expected - } - - "parse optional element" in { - val json = - """{ - | "name": "ze name", - | "simpleClass": { - | "value1": "value1", - | "value2": 42 - | } - |} - """.stripMargin - val result = getFromJSON[ComplexClass](json) - result.simpleClass.value.value1 mustEqual "value1" - result.simpleClass.value.value2 mustEqual 42 - - parseJSON(toJSON(result)) mustEqual parseJSON(json) - } - } - -} diff --git a/json/json-core/src/test/scala-3/io/sphere/json/generic/JsonTypeHintFieldSpec.scala b/json/json-core/src/test/scala-3/io/sphere/json/generic/JsonTypeHintFieldSpec.scala deleted file mode 100644 index 831176bc..00000000 --- a/json/json-core/src/test/scala-3/io/sphere/json/generic/JsonTypeHintFieldSpec.scala +++ /dev/null @@ -1,61 +0,0 @@ -package io.sphere.json.generic - -import cats.data.Validated.Valid -import io.sphere.json.* -import org.json4s.* -import org.scalatest.Inside -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class JsonTypeHintFieldSpec extends AnyWordSpec with Matchers with Inside { - import JsonTypeHintFieldSpec._ - - "JSONTypeHintField" must { - "allow to set another field to distinguish between types (toJValue)" in { - val user = UserWithPicture("foo-123", Medium, "http://example.com") - val expected = JObject( - List( - "userId" -> JString("foo-123"), - "pictureSize" -> JObject(List("pictureType" -> JString("Medium"))), - "pictureUrl" -> JString("http://example.com"))) - - val json = toJValue[UserWithPicture](user) - json must be(expected) - - inside(fromJValue[UserWithPicture](json)) { case Valid(parsedUser) => - parsedUser must be(user) - } - } - - "allow to set another field to distinguish between types (fromJSON)" in { - val json = - """ - { - "userId": "foo-123", - "pictureSize": { "pictureType": "Medium" }, - "pictureUrl": "http://example.com" - } - """ - - val Valid(user) = fromJSON[UserWithPicture](json): @unchecked - - user must be(UserWithPicture("foo-123", Medium, "http://example.com")) - } - } - -} - -object JsonTypeHintFieldSpec { - - @JSONTypeHintField(value = "pictureType") - sealed trait PictureSize - case object Small extends PictureSize - case object Medium extends PictureSize - case object Big extends PictureSize - - case class UserWithPicture(userId: String, pictureSize: PictureSize, pictureUrl: String) - - object UserWithPicture { - given JSON[UserWithPicture] = deriveJSON[UserWithPicture] - } -} diff --git a/json/json-derivation/src/test/scala-2/io/sphere/json/JSONEmbeddedSpec.scala b/json/json-derivation/src/test/scala-2/io/sphere/json/JSONEmbeddedSpec.scala deleted file mode 100644 index 459a8417..00000000 --- a/json/json-derivation/src/test/scala-2/io/sphere/json/JSONEmbeddedSpec.scala +++ /dev/null @@ -1,131 +0,0 @@ -package io.sphere.json - -import io.sphere.json.generic._ -import org.scalatest.OptionValues -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -object JSONEmbeddedSpec { - - case class Embedded(value1: String, value2: Int) - - object Embedded { - implicit val json: JSON[Embedded] = jsonProduct(apply _) - } - - case class Test1(name: String, @JSONEmbedded embedded: Embedded) - - object Test1 { - implicit val json: JSON[Test1] = jsonProduct(apply _) - } - - case class Test2(name: String, @JSONEmbedded embedded: Option[Embedded] = None) - - object Test2 { - implicit val json: JSON[Test2] = jsonProduct(apply _) - } - - case class SubTest4(@JSONEmbedded embedded: Embedded) - object SubTest4 { - implicit val json: JSON[SubTest4] = jsonProduct(apply _) - } - - case class Test4(subField: Option[SubTest4] = None) - object Test4 { - implicit val json: JSON[Test4] = jsonProduct(apply _) - } -} - -class JSONEmbeddedSpec extends AnyWordSpec with Matchers with OptionValues { - import JSONEmbeddedSpec._ - - "JSONEmbedded" should { - "flatten the json in one object" in { - val json = - """{ - | "name": "ze name", - | "value1": "ze value1", - | "value2": 45 - |} - """.stripMargin - val test1 = getFromJSON[Test1](json) - test1.name mustEqual "ze name" - test1.embedded.value1 mustEqual "ze value1" - test1.embedded.value2 mustEqual 45 - - val result = toJSON(test1) - parseJSON(result) mustEqual parseJSON(json) - } - - "validate that the json contains all needed fields" in { - val json = - """{ - | "name": "ze name", - | "value1": "ze value1" - |} - """.stripMargin - fromJSON[Test1](json).isInvalid must be(true) - fromJSON[Test1]("""{"name": "a"}""").isInvalid must be(true) - } - - "support optional embedded attribute" in { - val json = - """{ - | "name": "ze name", - | "value1": "ze value1", - | "value2": 45 - |} - """.stripMargin - val test2 = getFromJSON[Test2](json) - test2.name mustEqual "ze name" - test2.embedded.value.value1 mustEqual "ze value1" - test2.embedded.value.value2 mustEqual 45 - - val result = toJSON(test2) - parseJSON(result) mustEqual parseJSON(json) - } - - "ignore unknown fields" in { - val json = - """{ - | "name": "ze name", - | "value1": "ze value1", - | "value2": 45, - | "value3": true - |} - """.stripMargin - val test2 = getFromJSON[Test2](json) - test2.name mustEqual "ze name" - test2.embedded.value.value1 mustEqual "ze value1" - test2.embedded.value.value2 mustEqual 45 - } - - "check for sub-fields" in { - val json = - """ - { - "subField": { - "value1": "ze value1", - "value2": 45 - } - } - """ - val test4 = getFromJSON[Test4](json) - test4.subField.value.embedded.value1 mustEqual "ze value1" - test4.subField.value.embedded.value2 mustEqual 45 - } - - "support the absence of optional embedded attribute" in { - val json = """{ "name": "ze name" }""" - val test2 = getFromJSON[Test2](json) - test2.name mustEqual "ze name" - test2.embedded mustEqual None - } - - "validate the absence of some embedded attributes" in { - val json = """{ "name": "ze name", "value1": "ze value1" }""" - fromJSON[Test2](json).isInvalid must be(true) - } - } - -} diff --git a/json/json-derivation/src/test/scala-2/io/sphere/json/NullHandlingSpec.scala b/json/json-derivation/src/test/scala-2/io/sphere/json/NullHandlingSpec.scala deleted file mode 100644 index 3a11d3bb..00000000 --- a/json/json-derivation/src/test/scala-2/io/sphere/json/NullHandlingSpec.scala +++ /dev/null @@ -1,68 +0,0 @@ -package io.sphere.json - -import io.sphere.json.generic._ -import org.json4s.JsonAST.{JNothing, JObject} -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class NullHandlingSpec extends AnyWordSpec with Matchers { - "JSON deserialization" must { - "should accept undefined fields and use default values for them" in { - val jeans = getFromJSON[Jeans]("{}") - - jeans must be(Jeans(None, None, Set.empty, "secret")) - } - - "should accept null values and use default values for them" in { - val jeans = getFromJSON[Jeans](""" - { - "leftPocket": null, - "rightPocket": null, - "backPocket": null, - "hiddenPocket": null - } - """) - - jeans must be(Jeans(None, None, Set.empty, "secret")) - } - - "should accept JNothing values and use default values for them" in { - val jeans = getFromJValue[Jeans]( - JObject( - "leftPocket" -> JNothing, - "rightPocket" -> JNothing, - "backPocket" -> JNothing, - "hiddenPocket" -> JNothing)) - - jeans must be(Jeans(None, None, Set.empty, "secret")) - } - - "should accept not-null values and use them" in { - val jeans = getFromJSON[Jeans](""" - { - "leftPocket": "Axe", - "rightPocket": "Magic powder", - "backPocket": ["Magic wand", "Rusty sword"], - "hiddenPocket": "The potion of healing" - } - """) - - jeans must be( - Jeans( - Some("Axe"), - Some("Magic powder"), - Set("Magic wand", "Rusty sword"), - "The potion of healing")) - } - } -} - -case class Jeans( - leftPocket: Option[String] = None, - rightPocket: Option[String], - backPocket: Set[String] = Set.empty, - hiddenPocket: String = "secret") - -object Jeans { - implicit val json: JSON[Jeans] = deriveJSON[Jeans] -} diff --git a/json/json-derivation/src/test/scala-2/io/sphere/json/generic/DefaultValuesSpec.scala b/json/json-derivation/src/test/scala-2/io/sphere/json/generic/DefaultValuesSpec.scala deleted file mode 100644 index dd81878b..00000000 --- a/json/json-derivation/src/test/scala-2/io/sphere/json/generic/DefaultValuesSpec.scala +++ /dev/null @@ -1,33 +0,0 @@ -package io.sphere.mongo.generic - -import io.sphere.json._ -import io.sphere.json.generic._ -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class DefaultValuesSpec extends AnyWordSpec with Matchers { - import DefaultValuesSpec._ - - "deriving JSON" must { - "handle default values" in { - val json = "{}" - val test = getFromJSON[Test](json) - test.value1 must be("hello") - test.value2 must be(None) - test.value3 must be(None) - test.value4 must be(Some("hi")) - } - } -} - -object DefaultValuesSpec { - case class Test( - value1: String = "hello", - value2: Option[String], - value3: Option[String] = None, - value4: Option[String] = Some("hi") - ) - object Test { - implicit val mongo: JSON[Test] = jsonProduct(apply _) - } -} diff --git a/json/json-derivation/src/test/scala-2/io/sphere/json/generic/JSONKeySpec.scala b/json/json-derivation/src/test/scala-2/io/sphere/json/generic/JSONKeySpec.scala deleted file mode 100644 index 61a9add1..00000000 --- a/json/json-derivation/src/test/scala-2/io/sphere/json/generic/JSONKeySpec.scala +++ /dev/null @@ -1,45 +0,0 @@ -package io.sphere.mongo.generic - -import io.sphere.json._ -import io.sphere.json.generic._ -import org.json4s.DefaultReaders._ -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class JSONKeySpec extends AnyWordSpec with Matchers { - import JSONKeySpec._ - - "deriving JSON" must { - "rename fields annotated with @JSONKey" in { - val test = - Test(value1 = "value1", value2 = "value2", subTest = SubTest(value2 = "other_value2")) - - val json = toJValue(test) - (json \ "value1").as[Option[String]] must be(Some("value1")) - (json \ "value2").as[Option[String]] must be(None) - (json \ "new_value_2").as[Option[String]] must be(Some("value2")) - (json \ "new_sub_value_2").as[Option[String]] must be(Some("other_value2")) - - val newTest = getFromJValue[Test](json) - newTest must be(test) - } - } -} - -object JSONKeySpec { - case class SubTest( - @JSONKey("new_sub_value_2") value2: String - ) - object SubTest { - implicit val mongo: JSON[SubTest] = jsonProduct(apply _) - } - - case class Test( - value1: String, - @JSONKey("new_value_2") value2: String, - @JSONEmbedded subTest: SubTest - ) - object Test { - implicit val mongo: JSON[Test] = jsonProduct(apply _) - } -} diff --git a/json/json-core/src/test/scala-3/io/sphere/json/DeriveSingletonJSONSpec.scala b/json/json-derivation/src/test/scala-3/io/sphere/json/DeriveSingletonJSONSpec.scala similarity index 99% rename from json/json-core/src/test/scala-3/io/sphere/json/DeriveSingletonJSONSpec.scala rename to json/json-derivation/src/test/scala-3/io/sphere/json/DeriveSingletonJSONSpec.scala index 43468045..4df91ae2 100644 --- a/json/json-core/src/test/scala-3/io/sphere/json/DeriveSingletonJSONSpec.scala +++ b/json/json-derivation/src/test/scala-3/io/sphere/json/DeriveSingletonJSONSpec.scala @@ -146,6 +146,7 @@ case object Big extends PictureSize(1024, 2048) case object Custom extends PictureSize(1, 2) object PictureSize { + // TODO We have to get rid of this import DeriveSingleton.derived given JSON[PictureSize] = deriveSingletonJSON diff --git a/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala similarity index 100% rename from json/json-core/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala rename to json/json-derivation/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala diff --git a/json/json-core/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala similarity index 100% rename from json/json-core/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala rename to json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala diff --git a/json/json-core/src/test/scala-3/io/sphere/json/JSONEmbeddedSpec.scala b/json/json-derivation/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala similarity index 92% rename from json/json-core/src/test/scala-3/io/sphere/json/JSONEmbeddedSpec.scala rename to json/json-derivation/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala index f5c7eba4..7445ae57 100644 --- a/json/json-core/src/test/scala-3/io/sphere/json/JSONEmbeddedSpec.scala +++ b/json/json-derivation/src/test/scala/io/sphere/json/JSONEmbeddedSpec.scala @@ -1,38 +1,38 @@ package io.sphere.json +import io.sphere.json.generic._ import org.scalatest.OptionValues import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec -import io.sphere.json.generic._ object JSONEmbeddedSpec { case class Embedded(value1: String, value2: Int) object Embedded { - given JSON[Embedded] = deriveJSON[Embedded] + implicit val json: JSON[Embedded] = deriveJSON } case class Test1(name: String, @JSONEmbedded embedded: Embedded) object Test1 { - given JSON[Test1] = deriveJSON[Test1] + implicit val json: JSON[Test1] = deriveJSON } case class Test2(name: String, @JSONEmbedded embedded: Option[Embedded] = None) object Test2 { - given JSON[Test2] = deriveJSON + implicit val json: JSON[Test2] = deriveJSON } case class SubTest4(@JSONEmbedded embedded: Embedded) object SubTest4 { - given JSON[SubTest4] = deriveJSON + implicit val json: JSON[SubTest4] = deriveJSON } case class Test4(subField: Option[SubTest4] = None) object Test4 { - given JSON[Test4] = deriveJSON + implicit val json: JSON[Test4] = deriveJSON } } diff --git a/json/json-core/src/test/scala-3/io/sphere/json/NullHandlingSpec.scala b/json/json-derivation/src/test/scala/io/sphere/json/NullHandlingSpec.scala similarity index 97% rename from json/json-core/src/test/scala-3/io/sphere/json/NullHandlingSpec.scala rename to json/json-derivation/src/test/scala/io/sphere/json/NullHandlingSpec.scala index 5450d9e2..2a4d2e7d 100644 --- a/json/json-core/src/test/scala-3/io/sphere/json/NullHandlingSpec.scala +++ b/json/json-derivation/src/test/scala/io/sphere/json/NullHandlingSpec.scala @@ -64,5 +64,5 @@ case class Jeans( hiddenPocket: String = "secret") object Jeans { - given JSON[Jeans] = deriveJSON[Jeans] + implicit val json: JSON[Jeans] = deriveJSON[Jeans] } diff --git a/json/json-derivation/src/test/scala-2/io/sphere/json/OptionReaderSpec.scala b/json/json-derivation/src/test/scala/io/sphere/json/OptionReaderSpec.scala similarity index 94% rename from json/json-derivation/src/test/scala-2/io/sphere/json/OptionReaderSpec.scala rename to json/json-derivation/src/test/scala/io/sphere/json/OptionReaderSpec.scala index affb805e..6535d8ac 100644 --- a/json/json-derivation/src/test/scala-2/io/sphere/json/OptionReaderSpec.scala +++ b/json/json-derivation/src/test/scala/io/sphere/json/OptionReaderSpec.scala @@ -11,23 +11,23 @@ object OptionReaderSpec { case class SimpleClass(value1: String, value2: Int) object SimpleClass { - implicit val json: JSON[SimpleClass] = jsonProduct(apply _) + implicit val json: JSON[SimpleClass] = deriveJSON } case class ComplexClass(name: String, simpleClass: Option[SimpleClass]) object ComplexClass { - implicit val json: JSON[ComplexClass] = jsonProduct(apply _) + implicit val json: JSON[ComplexClass] = deriveJSON } case class MapClass(id: Long, map: Option[Map[String, String]]) object MapClass { - implicit val json: JSON[MapClass] = jsonProduct(apply _) + implicit val json: JSON[MapClass] = deriveJSON } case class ListClass(id: Long, list: Option[List[String]]) object ListClass { - implicit val json: JSON[ListClass] = jsonProduct(apply _) + implicit val json: JSON[ListClass] = deriveJSON } } diff --git a/json/json-core/src/test/scala-3/io/sphere/json/generic/DefaultValuesSpec.scala b/json/json-derivation/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala similarity index 89% rename from json/json-core/src/test/scala-3/io/sphere/json/generic/DefaultValuesSpec.scala rename to json/json-derivation/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala index 7bca45fe..e61459bd 100644 --- a/json/json-core/src/test/scala-3/io/sphere/json/generic/DefaultValuesSpec.scala +++ b/json/json-derivation/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala @@ -1,7 +1,7 @@ package io.sphere.json.generic + import io.sphere.json._ -import io.sphere.json.generic._ import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -32,13 +32,13 @@ object DefaultValuesSpec { value3: Option[String] = Some("hi") ) object Test { - given JSON[Test] = deriveJSON[Test] + implicit val json: JSON[Test] = deriveJSON[Test] } case class Test2( value1: String = "hello", value2: Option[String] ) object Test2 { - given JSON[Test2] = deriveJSON[Test2] + implicit val json: JSON[Test2] = deriveJSON[Test2] } } diff --git a/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONKeySpec.scala b/json/json-derivation/src/test/scala/io/sphere/json/generic/JSONKeySpec.scala similarity index 91% rename from json/json-core/src/test/scala-3/io/sphere/json/generic/JSONKeySpec.scala rename to json/json-derivation/src/test/scala/io/sphere/json/generic/JSONKeySpec.scala index df2bb582..f9630d80 100644 --- a/json/json-core/src/test/scala-3/io/sphere/json/generic/JSONKeySpec.scala +++ b/json/json-derivation/src/test/scala/io/sphere/json/generic/JSONKeySpec.scala @@ -3,7 +3,6 @@ package io.sphere.json.generic import org.json4s.MonadicJValue.jvalueToMonadic import org.json4s.jvalue2readerSyntax import io.sphere.json._ -import io.sphere.json.generic._ import org.json4s.DefaultReaders._ import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -33,7 +32,7 @@ object JSONKeySpec { @JSONKey("new_sub_value_2") value2: String ) object SubTest { - given JSON[SubTest] = deriveJSON[SubTest] + implicit val json: JSON[SubTest] = deriveJSON } case class Test( @@ -42,6 +41,6 @@ object JSONKeySpec { @JSONEmbedded subTest: SubTest ) object Test { - given JSON[Test] = deriveJSON[Test] + implicit val json: JSON[Test] = deriveJSON } } diff --git a/json/json-derivation/src/test/scala-2/io/sphere/json/generic/JsonTypeHintFieldSpec.scala b/json/json-derivation/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala similarity index 82% rename from json/json-derivation/src/test/scala-2/io/sphere/json/generic/JsonTypeHintFieldSpec.scala rename to json/json-derivation/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala index 41d60f94..fe8ff5ea 100644 --- a/json/json-derivation/src/test/scala-2/io/sphere/json/generic/JsonTypeHintFieldSpec.scala +++ b/json/json-derivation/src/test/scala/io/sphere/json/generic/JsonTypeHintFieldSpec.scala @@ -3,10 +3,11 @@ package io.sphere.json.generic import cats.data.Validated.Valid import io.sphere.json._ import org.json4s._ +import org.scalatest.Inside import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec -class JsonTypeHintFieldSpec extends AnyWordSpec with Matchers { +class JsonTypeHintFieldSpec extends AnyWordSpec with Matchers with Inside { import JsonTypeHintFieldSpec._ "JSONTypeHintField" must { @@ -20,9 +21,13 @@ class JsonTypeHintFieldSpec extends AnyWordSpec with Matchers { val json = toJValue[UserWithPicture](user) json must be(expected) + + inside(fromJValue[UserWithPicture](json)) { case Valid(parsedUser) => + parsedUser must be(user) + } } - "allow to set another field to distinguish between types (fromMongo)" in { + "allow to set another field to distinguish between types (fromJSON)" in { val json = """ { @@ -32,7 +37,7 @@ class JsonTypeHintFieldSpec extends AnyWordSpec with Matchers { } """ - val Valid(user) = fromJSON[UserWithPicture](json) + val user = fromJSON[UserWithPicture](json).getOrElse(null) user must be(UserWithPicture("foo-123", Medium, "http://example.com")) } @@ -58,6 +63,6 @@ object JsonTypeHintFieldSpec { case class UserWithPicture(userId: String, pictureSize: PictureSize, pictureUrl: String) object UserWithPicture { - implicit val json: JSON[UserWithPicture] = jsonProduct(apply _) + implicit val json: JSON[UserWithPicture] = deriveJSON } } From 9c586f5dc9d4ba7b025f90d867dfcae27f5d507e Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 8 May 2025 11:37:01 +0200 Subject: [PATCH 108/142] formatting --- .../test/scala/io/sphere/json/generic/DefaultValuesSpec.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/json/json-derivation/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala b/json/json-derivation/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala index e61459bd..ecf4b0b6 100644 --- a/json/json-derivation/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala +++ b/json/json-derivation/src/test/scala/io/sphere/json/generic/DefaultValuesSpec.scala @@ -1,6 +1,5 @@ package io.sphere.json.generic - import io.sphere.json._ import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec From da336cf611b6269c24db47d0397b90a95b9f452d Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 8 May 2025 11:54:04 +0200 Subject: [PATCH 109/142] trying to fix build --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index f55dea4d..a3bdbc59 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ lazy val scala3 = "3.3.5" // sbt-github-actions needs configuration in `ThisBuild` ThisBuild / crossScalaVersions := Seq(scala213, scala3) -ThisBuild / scalaVersion := scala3 +ThisBuild / scalaVersion := scala213 ThisBuild / githubWorkflowPublishTargetBranches := List() ThisBuild / githubWorkflowJavaVersions := List(JavaSpec.temurin("21")) ThisBuild / githubWorkflowBuildPreamble ++= List( From 89c4fe6c55e96e6e4c7df96a202604d090e14fda Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 8 May 2025 12:05:16 +0200 Subject: [PATCH 110/142] moving more mongo derivation tests to the common section --- .../io/sphere/mongo/format/MongoFormat.scala | 4 +- .../io/sphere/mongo/generic/generic.scala | 3 + .../mongo/format/OptionMongoFormatSpec.scala | 96 ------------ .../mongo/generic/DefaultValuesSpec.scala | 37 ----- .../mongo/generic/DeriveMongoFormatSpec.scala | 137 ------------------ .../mongo/generic/MongoEmbeddedSpec.scala | 124 ---------------- .../sphere/mongo/generic/MongoKeySpec.scala | 44 ------ ...goTypeHintFieldWithAbstractClassSpec.scala | 53 ------- ...ongoTypeHintFieldWithSealedTraitSpec.scala | 54 ------- .../io/sphere/mongo/DerivationSpec.scala | 4 +- .../io/sphere/mongo/SerializationTest.scala | 2 +- .../mongo/generic/MongoTypeSwitchSpec.scala | 0 .../mongo/generic/SumTypesDerivingSpec.scala | 3 +- .../io/sphere/mongo/MongoUtils.scala | 0 .../mongo/format/OptionMongoFormatSpec.scala | 8 +- .../mongo/generic/DefaultValuesSpec.scala | 2 +- .../mongo/generic/DeriveMongoformatSpec.scala | 7 +- .../mongo/generic/MongoEmbeddedSpec.scala | 12 +- .../mongo/generic/MongoIgnoreSpec.scala | 10 +- .../sphere/mongo/generic/MongoKeySpec.scala | 4 +- ...goTypeHintFieldWithAbstractClassSpec.scala | 2 +- ...ongoTypeHintFieldWithSealedTraitSpec.scala | 2 +- 22 files changed, 32 insertions(+), 576 deletions(-) delete mode 100644 mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/OptionMongoFormatSpec.scala delete mode 100644 mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/DefaultValuesSpec.scala delete mode 100644 mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala delete mode 100644 mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoEmbeddedSpec.scala delete mode 100644 mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoKeySpec.scala delete mode 100644 mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala delete mode 100644 mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala rename mongo/{mongo-core => mongo-derivation}/src/test/scala-3/io/sphere/mongo/DerivationSpec.scala (91%) rename mongo/{mongo-core => mongo-derivation}/src/test/scala-3/io/sphere/mongo/SerializationTest.scala (98%) rename mongo/{mongo-core => mongo-derivation}/src/test/scala-3/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala (100%) rename mongo/{mongo-core => mongo-derivation}/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala (99%) rename mongo/mongo-derivation/src/test/{scala-2 => scala}/io/sphere/mongo/MongoUtils.scala (100%) rename mongo/mongo-derivation/src/test/{scala-2 => scala}/io/sphere/mongo/format/OptionMongoFormatSpec.scala (93%) rename mongo/mongo-derivation/src/test/{scala-2 => scala}/io/sphere/mongo/generic/DefaultValuesSpec.scala (92%) rename mongo/mongo-derivation/src/test/{scala-2 => scala}/io/sphere/mongo/generic/DeriveMongoformatSpec.scala (97%) rename mongo/mongo-derivation/src/test/{scala-2 => scala}/io/sphere/mongo/generic/MongoEmbeddedSpec.scala (90%) rename mongo/{mongo-core/src/test/scala-3 => mongo-derivation/src/test/scala}/io/sphere/mongo/generic/MongoIgnoreSpec.scala (77%) rename mongo/mongo-derivation/src/test/{scala-2 => scala}/io/sphere/mongo/generic/MongoKeySpec.scala (90%) rename mongo/mongo-derivation/src/test/{scala-2 => scala}/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala (95%) rename mongo/mongo-derivation/src/test/{scala-2 => scala}/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala (96%) diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala index d6424575..10eedab9 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala @@ -45,8 +45,6 @@ object TraitMongoFormat { } } -inline def deriveMongoFormat[A](using Mirror.Of[A]): MongoFormat[A] = MongoFormat.derived - object MongoFormat { private val emptyFields: Set[String] = Set.empty @@ -155,7 +153,7 @@ object MongoFormat { if (field.ignored) defaultValue.getOrElse { throw new Exception( - s"Missing default parameter value for ignored field `${field.name}` on deserialization.") + s"Ignored Mongo field '${field.name}' must have a default value.") } else if (field.embedded) format.fromMongoValue(bson) else { diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala index 4a0590af..50c6e671 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala @@ -4,8 +4,11 @@ import com.mongodb.BasicDBObject import io.sphere.mongo.format.MongoFormat import org.bson.BSONObject +import scala.deriving.Mirror import scala.compiletime.{erasedValue, error, summonInline} +inline def deriveMongoFormat[A](using Mirror.Of[A]): MongoFormat[A] = MongoFormat.derived + def mongoEnum(e: Enumeration): MongoFormat[e.Value] = new MongoFormat[e.Value] { def toMongoValue(a: e.Value): Any = a.toString diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/OptionMongoFormatSpec.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/OptionMongoFormatSpec.scala deleted file mode 100644 index 5f3055af..00000000 --- a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/format/OptionMongoFormatSpec.scala +++ /dev/null @@ -1,96 +0,0 @@ -package io.sphere.mongo.format - -import io.sphere.mongo.MongoUtils.* -import io.sphere.mongo.generic.* -import DefaultMongoFormats.given -import org.scalatest.OptionValues -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -object OptionMongoFormatSpec { - - case class SimpleClass(value1: String, value2: Int) - - object SimpleClass { - given MongoFormat[SimpleClass] = deriveMongoFormat[SimpleClass] - } - - case class ComplexClass(name: String, simpleClass: Option[SimpleClass]) - - object ComplexClass { - given MongoFormat[ComplexClass] = deriveMongoFormat[ComplexClass] - } - -} - -class OptionMongoFormatSpec extends AnyWordSpec with Matchers with OptionValues { - import OptionMongoFormatSpec.* - - "MongoFormat[Option[_]]" should { - "handle presence of all fields" in { - val dbo = dbObj( - "value1" -> "a", - "value2" -> 45 - ) - val result = MongoFormat[Option[SimpleClass]].fromMongoValue(dbo) - result.value.value1 mustEqual "a" - result.value.value2 mustEqual 45 - } - - "handle presence of all fields mixed with ignored fields" in { - val dbo = dbObj( - "value1" -> "a", - "value2" -> 45, - "value3" -> "b" - ) - val result = MongoFormat[Option[SimpleClass]].fromMongoValue(dbo) - result.value.value1 mustEqual "a" - result.value.value2 mustEqual 45 - } - - "handle presence of not all the fields" in { - val dbo = dbObj("value1" -> "a") - an[Exception] mustBe thrownBy(MongoFormat[Option[SimpleClass]].fromMongoValue(dbo)) - } - - "handle absence of all fields" in { - val dbo = dbObj() - val result = MongoFormat[Option[SimpleClass]].fromMongoValue(dbo) - result mustEqual None - } - - "handle absence of all fields mixed with ignored fields" in { - val dbo = dbObj("value3" -> "a") - val result = MongoFormat[Option[SimpleClass]].fromMongoValue(dbo) - result mustEqual None - } - - "consider all fields if the data type does not impose any restriction" in { - val dbo = dbObj( - "key1" -> "value1", - "key2" -> "value2" - ) - val expected = Map("key1" -> "value1", "key2" -> "value2") - val result = MongoFormat[Map[String, String]].fromMongoValue(dbo) - result mustEqual expected - - val maybeResult = MongoFormat[Option[Map[String, String]]].fromMongoValue(dbo) - maybeResult.value mustEqual expected - } - - "parse optional element" in { - val dbo = dbObj( - "name" -> "ze name", - "simpleClass" -> dbObj( - "value1" -> "value1", - "value2" -> 42 - ) - ) - val result = MongoFormat[ComplexClass].fromMongoValue(dbo) - result.simpleClass.value.value1 mustEqual "value1" - result.simpleClass.value.value2 mustEqual 42 - - MongoFormat[ComplexClass].toMongoValue(result) mustEqual dbo - } - } -} diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/DefaultValuesSpec.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/DefaultValuesSpec.scala deleted file mode 100644 index e481fff7..00000000 --- a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/DefaultValuesSpec.scala +++ /dev/null @@ -1,37 +0,0 @@ -package io.sphere.mongo.generic - -import io.sphere.mongo.MongoUtils.* -import io.sphere.mongo.format.DefaultMongoFormats.given -import io.sphere.mongo.format.{MongoFormat, deriveMongoFormat} -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class DefaultValuesSpec extends AnyWordSpec with Matchers { - - import DefaultValuesSpec.* - - "deriving TypedMongoFormat" must { - "handle default values" in { - val dbo = dbObj() - val test = MongoFormat[CaseClass].fromMongoValue(dbo) - import test._ - field1 mustBe "hello" - field2 mustBe None - field3 mustBe None - field4 mustBe Some("hi") - } - } -} - -object DefaultValuesSpec { - case class CaseClass( - field1: String = "hello", - field2: Option[String], - field3: Option[String] = None, - field4: Option[String] = Some("hi") - ) - - object CaseClass { - given MongoFormat[CaseClass] = deriveMongoFormat[CaseClass] - } -} diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala deleted file mode 100644 index 7c96930e..00000000 --- a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/DeriveMongoFormatSpec.scala +++ /dev/null @@ -1,137 +0,0 @@ -package io.sphere.mongo.generic - -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec -import io.sphere.mongo.format.DefaultMongoFormats.given -import io.sphere.mongo.MongoUtils.* -import io.sphere.mongo.format.{MongoFormat, deriveMongoFormat} -import io.sphere.mongo.format.{fromMongo, toMongo} - -class DeriveMongoFormatSpec extends AnyWordSpec with Matchers { - import DeriveMongoFormatSpec.given - import DeriveMongoFormatSpec.* - - "deriving MongoFormat" must { - "read normal singleton values" in { - val user = fromMongo[UserWithPicture]( - dbObj( - "userId" -> "foo-123", - "pictureSize" -> dbObj("type" -> "Medium"), - "pictureUrl" -> "http://example.com")) - - user must be(UserWithPicture("foo-123", Medium, "http://example.com")) - } - - "read custom singleton values" in { - val user = fromMongo[UserWithPicture]( - dbObj( - "userId" -> "foo-123", - "pictureSize" -> dbObj("type" -> "bar", "width" -> 23, "height" -> 30), - "pictureUrl" -> "http://example.com")) - - user must be(UserWithPicture("foo-123", Custom(23, 30), "http://example.com")) - } - - "fail to read if singleton value is unknown" in { - a[Exception] must be thrownBy fromMongo[UserWithPicture]( - dbObj( - "userId" -> "foo-123", - "pictureSize" -> dbObj("type" -> "Unknown"), - "pictureUrl" -> "http://example.com")) - } - - "write normal singleton values" in { - val dbo = toMongo[UserWithPicture](UserWithPicture("foo-123", Medium, "http://example.com")) - dbo must be( - dbObj( - "userId" -> "foo-123", - "pictureSize" -> dbObj("type" -> "Medium"), - "pictureUrl" -> "http://example.com")) - } - - "write custom singleton values" in { - val dbo = - toMongo[UserWithPicture](UserWithPicture("foo-123", Custom(23, 30), "http://example.com")) - dbo must be( - dbObj( - "userId" -> "foo-123", - "pictureSize" -> dbObj("type" -> "bar", "width" -> 23, "height" -> 30), - "pictureUrl" -> "http://example.com")) - } - - "write and consequently read, which must produce the original value" in { - val originalUser = UserWithPicture("foo-123", Medium, "http://exmple.com") - val newUser = fromMongo[UserWithPicture](toMongo[UserWithPicture](originalUser)) - - newUser must be(originalUser) - } - - "read and write sealed trait with only one subtype" in { - val dbo = dbObj( - "userId" -> "foo-123", - "pictureSize" -> dbObj("type" -> "Medium"), - "pictureUrl" -> "http://example.com", - "access" -> dbObj("type" -> "Authorized", "project" -> "internal") - ) - val user = fromMongo[UserWithPicture](dbo) - - user must be( - UserWithPicture( - "foo-123", - Medium, - "http://example.com", - Some(Access.Authorized("internal")))) - val newDbo = toMongo[UserWithPicture](user) - newDbo must be(dbo) - - val newUser = fromMongo[UserWithPicture](newDbo) - newUser must be(user) - } - - "fail to derive if trait is not sealed" in { - // Sealed - "implicit val mongo: MongoFormat[SealedSub] = deriveMongoFormat[SealedSub]" must compile - // Not sealed - "implicit val mongo: MongoFormat[NotSealed] = deriveMongoFormat[NotSealed]" mustNot compile - // Sealed, but child is not sealed - "implicit val mongo: MongoFormat[SealedParent] = deriveMongoFormat[SealedParent]" mustNot compile - } - } -} - -object DeriveMongoFormatSpec { - sealed trait PictureSize - case object Small extends PictureSize - case object Medium extends PictureSize - case object Big extends PictureSize - @MongoTypeHint(value = "bar") - case class Custom(width: Int, height: Int) extends PictureSize - - given MongoFormat[PictureSize] = deriveMongoFormat[PictureSize] - - sealed trait Access - object Access { - // only one sub-type - case class Authorized(project: String) extends Access - - } - given MongoFormat[Access] = deriveMongoFormat - - case class UserWithPicture( - userId: String, - pictureSize: PictureSize, - pictureUrl: String, - access: Option[Access] = None) - - given MongoFormat[UserWithPicture] = deriveMongoFormat - - sealed trait SealedParent - - sealed trait SealedSub extends SealedParent - case class Sub1(x: String) extends SealedSub - case class Sub2(y: Int) extends SealedSub - - trait NotSealed extends SealedParent - case class Sub3(x: String) extends NotSealed - case class Sub4(y: Int) extends NotSealed -} diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoEmbeddedSpec.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoEmbeddedSpec.scala deleted file mode 100644 index b46493c6..00000000 --- a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoEmbeddedSpec.scala +++ /dev/null @@ -1,124 +0,0 @@ -package io.sphere.mongo.generic - -import io.sphere.mongo.MongoUtils.* -import io.sphere.mongo.format.deriveMongoFormat -import io.sphere.mongo.format.DefaultMongoFormats.given -import org.scalatest.OptionValues -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -import scala.util.Try - -object MongoEmbeddedSpec { - private case class Embedded(value1: String, @MongoKey("_value2") value2: Int) - - private case class Test1(name: String, @MongoEmbedded embedded: Embedded) - - private case class Test2(name: String, @MongoEmbedded embedded: Option[Embedded] = None) - - private case class ClassWithMongoIgnore( - @MongoIgnore name: String = "default", - @MongoEmbedded embedded: Option[Embedded] = None) - - private case class SubTest4(@MongoEmbedded embedded: Embedded) - - private case class Test4(subField: Option[SubTest4] = None) -} - -class MongoEmbeddedSpec extends AnyWordSpec with Matchers with OptionValues { - import MongoEmbeddedSpec.* - - "MongoEmbedded" should { - "flatten the db object in one object" in { - val dbo = dbObj( - "name" -> "ze name", - "value1" -> "ze value1", - "_value2" -> 45 - ) - val test1 = deriveMongoFormat[Test1].fromMongoValue(dbo) - test1.name mustEqual "ze name" - test1.embedded.value1 mustEqual "ze value1" - test1.embedded.value2 mustEqual 45 - - val result = deriveMongoFormat[Test1].toMongoValue(test1) - result mustEqual dbo - } - - "validate that the db object contains all needed fields" in { - // TODO default field - val dbo = dbObj( - "name" -> "ze name", - "value1" -> "ze value1" - ) - Try(deriveMongoFormat[Test1].fromMongoValue(dbo)).isFailure must be(true) - } - - "support optional embedded attribute" in { - val dbo = dbObj( - "name" -> "ze name", - "value1" -> "ze value1", - "_value2" -> 45 - ) - val test2 = deriveMongoFormat[Test2].fromMongoValue(dbo) - test2.name mustEqual "ze name" - test2.embedded.value.value1 mustEqual "ze value1" - test2.embedded.value.value2 mustEqual 45 - - val result = deriveMongoFormat[Test2].toMongoValue(test2) - result mustEqual dbo - } - - "ignore unknown fields" in { - val dbo = dbObj( - "name" -> "ze name", - "value1" -> "ze value1", - "_value2" -> 45, - "value4" -> true - ) - val test2 = deriveMongoFormat[Test2].fromMongoValue(dbo) - test2.name mustEqual "ze name" - test2.embedded.value.value1 mustEqual "ze value1" - test2.embedded.value.value2 mustEqual 45 - } - - "ignore ignored fields" in { - val dbo = dbObj( - "value1" -> "ze value1", - "_value2" -> 45 - ) - val test3 = deriveMongoFormat[ClassWithMongoIgnore].fromMongoValue(dbo) - test3.name mustEqual "default" - test3.embedded.value.value1 mustEqual "ze value1" - test3.embedded.value.value2 mustEqual 45 - } - - "check for sub-fields" in { - val dbo = dbObj( - "subField" -> dbObj( - "value1" -> "ze value1", - "_value2" -> 45 - ) - ) - val test4 = deriveMongoFormat[Test4].fromMongoValue(dbo) - test4.subField.value.embedded.value1 mustEqual "ze value1" - test4.subField.value.embedded.value2 mustEqual 45 - } - - "support the absence of optional embedded attribute" in { - val dbo = dbObj( - "name" -> "ze name" - ) - val test2 = deriveMongoFormat[Test2].fromMongoValue(dbo) - test2.name mustBe "ze name" - test2.embedded mustBe None - } - - "validate the absence of some embedded attributes" in { - val dbo = dbObj( - "name" -> "ze name", - "value1" -> "ze value1" - ) - Try(deriveMongoFormat[Test2].fromMongoValue(dbo)).isFailure must be(true) - } - } -} diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoKeySpec.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoKeySpec.scala deleted file mode 100644 index a51d3d98..00000000 --- a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoKeySpec.scala +++ /dev/null @@ -1,44 +0,0 @@ -package io.sphere.mongo.generic - -import com.mongodb.BasicDBObject -import io.sphere.mongo.format.deriveMongoFormat -import io.sphere.mongo.format.DefaultMongoFormats.given -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -import scala.jdk.CollectionConverters.* - -class MongoKeySpec extends AnyWordSpec with Matchers { - import MongoKeySpec.* - - "deriving MongoFormat" must { - "rename fields annotated with @MongoKey" in { - val test = - Test(value1 = "value1", value2 = "value2", subTest = SubTest(value2 = "other_value2")) - - val formatter = deriveMongoFormat[Test] - val dbo = formatter.toMongoValue(test) - val map = dbo.asInstanceOf[BasicDBObject].toMap.asScala.toMap[Any, Any] - - map.get("value1") must equal(Some("value1")) - map.get("value2") must equal(None) - map.get("new_value_2") must equal(Some("value2")) - map.get("new_sub_value_2") must equal(Some("other_value2")) - - val newTest = formatter.fromMongoValue(dbo) - newTest must be(test) - } - } -} - -object MongoKeySpec { - case class SubTest( - @MongoKey("new_sub_value_2") value2: String - ) - - case class Test( - value1: String, - @MongoKey("new_value_2") value2: String, - @MongoEmbedded subTest: SubTest - ) -} diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala deleted file mode 100644 index db7e22c6..00000000 --- a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala +++ /dev/null @@ -1,53 +0,0 @@ -package io.sphere.mongo.generic - -import io.sphere.mongo.MongoUtils.dbObj -import io.sphere.mongo.format.{MongoFormat, deriveMongoFormat} -import io.sphere.mongo.format.DefaultMongoFormats.given -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class MongoTypeHintFieldWithAbstractClassSpec extends AnyWordSpec with Matchers { - import MongoTypeHintFieldWithAbstractClassSpec.* - - "MongoTypeHintField (with abstract class)" must { - "allow to set another field to distinguish between types (toMongo)" in { - val user = UserWithPicture("foo-123", Medium, "http://example.com") - val expected = dbObj( - "userId" -> "foo-123", - "pictureSize" -> dbObj("pictureType" -> "Medium"), - "pictureUrl" -> "http://example.com") - - val dbo = UserWithPicture.mongo.toMongoValue(user) - dbo must be(expected) - } - - "allow to set another field to distinguish between types (fromMongo)" in { - val initialDbo = dbObj( - "userId" -> "foo-123", - "pictureSize" -> dbObj("pictureType" -> "Medium"), - "pictureUrl" -> "http://example.com") - - val user = UserWithPicture.mongo.fromMongoValue(initialDbo) - - user must be(UserWithPicture("foo-123", Medium, "http://example.com")) - - val dbo = UserWithPicture.mongo.toMongoValue(user) - dbo must be(initialDbo) - } - } -} - -object MongoTypeHintFieldWithAbstractClassSpec { - - @MongoTypeHintField(value = "pictureType") - sealed abstract class PictureSize - case object Small extends PictureSize - case object Medium extends PictureSize - case object Big extends PictureSize - - case class UserWithPicture(userId: String, pictureSize: PictureSize, pictureUrl: String) - - object UserWithPicture { - val mongo: MongoFormat[UserWithPicture] = deriveMongoFormat[UserWithPicture] - } -} diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala b/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala deleted file mode 100644 index 49a61fb4..00000000 --- a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala +++ /dev/null @@ -1,54 +0,0 @@ -package io.sphere.mongo.generic - -import io.sphere.mongo.MongoUtils.dbObj -import io.sphere.mongo.format.{MongoFormat, deriveMongoFormat} -import io.sphere.mongo.format.DefaultMongoFormats.given -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class MongoTypeHintFieldWithSealedTraitSpec extends AnyWordSpec with Matchers { - import MongoTypeHintFieldWithSealedTraitSpec.* - - "MongoTypeHintField (with sealed trait)" must { - "allow to set another field to distinguish between types (toMongo)" in { - val user = UserWithPicture("foo-123", Medium, "http://example.com") - val expected = dbObj( - "userId" -> "foo-123", - "pictureSize" -> dbObj("pictureType" -> "Medium"), - "pictureUrl" -> "http://example.com") - - val dbo = userWithPictureFormat.toMongoValue(user) - dbo must be(expected) - } - - "allow to set another field to distinguish between types (fromMongo)" in { - val initialDbo = dbObj( - "userId" -> "foo-123", - "pictureSize" -> dbObj("pictureType" -> "Medium"), - "pictureUrl" -> "http://example.com") - - val user = userWithPictureFormat.fromMongoValue(initialDbo) - - user must be(UserWithPicture("foo-123", Medium, "http://example.com")) - - val dbo = userWithPictureFormat.toMongoValue(user) - dbo must be(initialDbo) - } - } -} - -object MongoTypeHintFieldWithSealedTraitSpec { - - // issue https://github.com/commercetools/sphere-scala-libs/issues/10 - // @MongoTypeHintField must be repeated for all sub-classes - @MongoTypeHintField(value = "pictureType") - sealed trait PictureSize - case object Small extends PictureSize - case object Medium extends PictureSize - case object Big extends PictureSize - - case class UserWithPicture(userId: String, pictureSize: PictureSize, pictureUrl: String) - - val userWithPictureFormat = deriveMongoFormat[UserWithPicture] - -} diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/DerivationSpec.scala b/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/DerivationSpec.scala similarity index 91% rename from mongo/mongo-core/src/test/scala-3/io/sphere/mongo/DerivationSpec.scala rename to mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/DerivationSpec.scala index 29e69458..692fc4fa 100644 --- a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/DerivationSpec.scala +++ b/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/DerivationSpec.scala @@ -12,7 +12,7 @@ class DerivationSpec extends AnyWordSpec with Matchers { case class Container(i: Int, str: String, component: Component) case class Component(i: Int) - val format = io.sphere.mongo.format.deriveMongoFormat[Container] + val format = io.sphere.mongo.generic.deriveMongoFormat[Container] val container = Container(123, "anything", Component(456)) val bson = format.toMongoValue(container) @@ -27,7 +27,7 @@ class DerivationSpec extends AnyWordSpec with Matchers { case object Object2 extends Root case class Class(i: Int) extends Root - val format = io.sphere.mongo.format.deriveMongoFormat[Root] + val format = io.sphere.mongo.generic.deriveMongoFormat[Root] def roundtrip(member: Root): Unit = { val bson = format.toMongoValue(member) diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/SerializationTest.scala b/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/SerializationTest.scala similarity index 98% rename from mongo/mongo-core/src/test/scala-3/io/sphere/mongo/SerializationTest.scala rename to mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/SerializationTest.scala index cef6dd0a..4b309d5f 100644 --- a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/SerializationTest.scala +++ b/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/SerializationTest.scala @@ -49,7 +49,7 @@ class SerializationTest extends AnyWordSpec with Matchers { dbo.put("b", Integer.valueOf(4)) // Using backwards-compatible `deriveMongoFormat` + `implicit` - implicit val x: MongoFormat[Something] = io.sphere.mongo.format.deriveMongoFormat + implicit val x: MongoFormat[Something] = io.sphere.mongo.generic.deriveMongoFormat val something = MongoFormat[Something].fromMongoValue(dbo) something mustBe Something(Some(3), 4) diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala b/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala similarity index 100% rename from mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala rename to mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala b/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala similarity index 99% rename from mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala rename to mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala index 41ab1daa..d377f50e 100644 --- a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala +++ b/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala @@ -2,7 +2,8 @@ package io.sphere.mongo.generic import com.mongodb.DBObject import io.sphere.mongo.MongoUtils.dbObj -import io.sphere.mongo.format.{MongoFormat, deriveMongoFormat} +import io.sphere.mongo.generic.deriveMongoFormat +import io.sphere.mongo.format.{MongoFormat} import io.sphere.mongo.format.DefaultMongoFormats.given import org.bson.BSONObject import org.scalatest.Assertion diff --git a/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/MongoUtils.scala b/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/MongoUtils.scala similarity index 100% rename from mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/MongoUtils.scala rename to mongo/mongo-derivation/src/test/scala/io/sphere/mongo/MongoUtils.scala diff --git a/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/format/OptionMongoFormatSpec.scala b/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala similarity index 93% rename from mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/format/OptionMongoFormatSpec.scala rename to mongo/mongo-derivation/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala index daf324c9..09703b95 100644 --- a/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/format/OptionMongoFormatSpec.scala +++ b/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/format/OptionMongoFormatSpec.scala @@ -1,24 +1,24 @@ package io.sphere.mongo.format +import io.sphere.mongo.MongoUtils._ +import io.sphere.mongo.format.DefaultMongoFormats._ import io.sphere.mongo.generic._ import org.scalatest.OptionValues import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec -import io.sphere.mongo.MongoUtils._ -import DefaultMongoFormats._ object OptionMongoFormatSpec { case class SimpleClass(value1: String, value2: Int) object SimpleClass { - implicit val mongo: MongoFormat[SimpleClass] = mongoProduct(apply _) + implicit val mongo: MongoFormat[SimpleClass] = deriveMongoFormat } case class ComplexClass(name: String, simpleClass: Option[SimpleClass]) object ComplexClass { - implicit val mongo: MongoFormat[ComplexClass] = mongoProduct(apply _) + implicit val mongo: MongoFormat[ComplexClass] = deriveMongoFormat } } diff --git a/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/DefaultValuesSpec.scala b/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala similarity index 92% rename from mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/DefaultValuesSpec.scala rename to mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala index 2d016857..03e7dd2d 100644 --- a/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/DefaultValuesSpec.scala +++ b/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/DefaultValuesSpec.scala @@ -29,6 +29,6 @@ object DefaultValuesSpec { value4: Option[String] = Some("hi") ) object Test { - implicit val mongo: MongoFormat[Test] = mongoProduct(apply _) + implicit val mongo: MongoFormat[Test] = deriveMongoFormat } } diff --git a/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/DeriveMongoformatSpec.scala b/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/DeriveMongoformatSpec.scala similarity index 97% rename from mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/DeriveMongoformatSpec.scala rename to mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/DeriveMongoformatSpec.scala index fcf3e559..67320d1d 100644 --- a/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/DeriveMongoformatSpec.scala +++ b/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/DeriveMongoformatSpec.scala @@ -1,11 +1,10 @@ package io.sphere.mongo.generic -import io.sphere.mongo.format.MongoFormat +import io.sphere.mongo.MongoUtils._ import io.sphere.mongo.format._ +import io.sphere.mongo.format.DefaultMongoFormats._ import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec -import io.sphere.mongo.format.DefaultMongoFormats._ -import io.sphere.mongo.MongoUtils._ class DeriveMongoformatSpec extends AnyWordSpec with Matchers { import DeriveMongoformatSpec._ @@ -125,7 +124,7 @@ object DeriveMongoformatSpec { access: Option[Access] = None) object UserWithPicture { - implicit val mongo: MongoFormat[UserWithPicture] = mongoProduct(apply _) + implicit val mongo: MongoFormat[UserWithPicture] = deriveMongoFormat } sealed trait SealedParent diff --git a/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/MongoEmbeddedSpec.scala b/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala similarity index 90% rename from mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/MongoEmbeddedSpec.scala rename to mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala index 1483da9d..12d1b52a 100644 --- a/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/MongoEmbeddedSpec.scala +++ b/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala @@ -13,19 +13,19 @@ object MongoEmbeddedSpec { case class Embedded(value1: String, @MongoKey("_value2") value2: Int) object Embedded { - implicit val mongo: MongoFormat[Embedded] = mongoProduct(apply _) + implicit val mongo: MongoFormat[Embedded] = deriveMongoFormat } case class Test1(name: String, @MongoEmbedded embedded: Embedded) object Test1 { - implicit val mongo: MongoFormat[Test1] = mongoProduct(apply _) + implicit val mongo: MongoFormat[Test1] = deriveMongoFormat } case class Test2(name: String, @MongoEmbedded embedded: Option[Embedded] = None) object Test2 { - implicit val mongo: MongoFormat[Test2] = mongoProduct(apply _) + implicit val mongo: MongoFormat[Test2] = deriveMongoFormat } case class Test3( @@ -33,17 +33,17 @@ object MongoEmbeddedSpec { @MongoEmbedded embedded: Option[Embedded] = None) object Test3 { - implicit val mongo: MongoFormat[Test3] = mongoProduct(apply _) + implicit val mongo: MongoFormat[Test3] = deriveMongoFormat } case class SubTest4(@MongoEmbedded embedded: Embedded) object SubTest4 { - implicit val mongo: MongoFormat[SubTest4] = mongoProduct(apply _) + implicit val mongo: MongoFormat[SubTest4] = deriveMongoFormat } case class Test4(subField: Option[SubTest4] = None) object Test4 { - implicit val mongo: MongoFormat[Test4] = mongoProduct(apply _) + implicit val mongo: MongoFormat[Test4] = deriveMongoFormat } } diff --git a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoIgnoreSpec.scala b/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoIgnoreSpec.scala similarity index 77% rename from mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoIgnoreSpec.scala rename to mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoIgnoreSpec.scala index 58bedee2..39c06d5a 100644 --- a/mongo/mongo-core/src/test/scala-3/io/sphere/mongo/generic/MongoIgnoreSpec.scala +++ b/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoIgnoreSpec.scala @@ -1,8 +1,8 @@ package io.sphere.mongo.generic -import io.sphere.mongo.MongoUtils.* -import io.sphere.mongo.format.deriveMongoFormat -import io.sphere.mongo.format.DefaultMongoFormats.given +import io.sphere.mongo.MongoUtils._ +import io.sphere.mongo.generic.deriveMongoFormat +import io.sphere.mongo.format.DefaultMongoFormats._ import org.scalatest.OptionValues import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -18,13 +18,13 @@ object MongoIgnoreSpec { } class MongoIgnoreSpec extends AnyWordSpec with Matchers with OptionValues { - import MongoIgnoreSpec.* + import MongoIgnoreSpec._ "MongoIgnore" when { "annotated field has no default" must { "fail with a suitable message" in { val e = the[Exception] thrownBy deriveMongoFormat[MissingDefault].fromMongoValue(dbo) - e.getMessage mustBe "Missing default parameter value for ignored field `age` on deserialization." + e.getMessage mustBe "Ignored Mongo field 'age' must have a default value." } } "annotated field has also a default" must { diff --git a/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/MongoKeySpec.scala b/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala similarity index 90% rename from mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/MongoKeySpec.scala rename to mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala index 330dac5b..2edd6d21 100644 --- a/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/MongoKeySpec.scala +++ b/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoKeySpec.scala @@ -34,7 +34,7 @@ object MongoKeySpec { @MongoKey("new_sub_value_2") value2: String ) object SubTest { - implicit val mongo: MongoFormat[SubTest] = mongoProduct(apply _) + implicit val mongo: MongoFormat[SubTest] = deriveMongoFormat } case class Test( @@ -43,6 +43,6 @@ object MongoKeySpec { @MongoEmbedded subTest: SubTest ) object Test { - implicit val mongo: MongoFormat[Test] = mongoProduct(apply _) + implicit val mongo: MongoFormat[Test] = deriveMongoFormat } } diff --git a/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala b/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala similarity index 95% rename from mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala rename to mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala index e78b98c5..ffc923eb 100644 --- a/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala +++ b/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithAbstractClassSpec.scala @@ -52,6 +52,6 @@ object MongoTypeHintFieldWithAbstractClassSpec { case class UserWithPicture(userId: String, pictureSize: PictureSize, pictureUrl: String) object UserWithPicture { - implicit val mongo: MongoFormat[UserWithPicture] = mongoProduct(apply _) + implicit val mongo: MongoFormat[UserWithPicture] = deriveMongoFormat } } diff --git a/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala b/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala similarity index 96% rename from mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala rename to mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala index 558906b3..ef91fdd1 100644 --- a/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala +++ b/mongo/mongo-derivation/src/test/scala/io/sphere/mongo/generic/MongoTypeHintFieldWithSealedTraitSpec.scala @@ -57,6 +57,6 @@ object MongoTypeHintFieldWithSealedTraitSpec { case class UserWithPicture(userId: String, pictureSize: PictureSize, pictureUrl: String) object UserWithPicture { - implicit val mongo: MongoFormat[UserWithPicture] = mongoProduct(apply _) + implicit val mongo: MongoFormat[UserWithPicture] = deriveMongoFormat } } From b8e86b697bf24dbb5ab06095dad0d38dfa39a393 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 8 May 2025 12:06:36 +0200 Subject: [PATCH 111/142] move scala2 mongo derivation to its proper folder --- .../io/sphere/mongo/generic/MongoFormatMacros.scala | 0 .../{scala => scala-2}/io/sphere/mongo/generic/package.fmpp.scala | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename mongo/mongo-derivation/src/main/{scala => scala-2}/io/sphere/mongo/generic/MongoFormatMacros.scala (100%) rename mongo/mongo-derivation/src/main/{scala => scala-2}/io/sphere/mongo/generic/package.fmpp.scala (100%) diff --git a/mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/MongoFormatMacros.scala b/mongo/mongo-derivation/src/main/scala-2/io/sphere/mongo/generic/MongoFormatMacros.scala similarity index 100% rename from mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/MongoFormatMacros.scala rename to mongo/mongo-derivation/src/main/scala-2/io/sphere/mongo/generic/MongoFormatMacros.scala diff --git a/mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/package.fmpp.scala b/mongo/mongo-derivation/src/main/scala-2/io/sphere/mongo/generic/package.fmpp.scala similarity index 100% rename from mongo/mongo-derivation/src/main/scala/io/sphere/mongo/generic/package.fmpp.scala rename to mongo/mongo-derivation/src/main/scala-2/io/sphere/mongo/generic/package.fmpp.scala From e08cd684de156ee7086d50e44aa818cea308f0c9 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 8 May 2025 12:07:34 +0200 Subject: [PATCH 112/142] move scala2 json derivation to its proper folder --- .../{scala => scala-2}/io/sphere/json/ToJSONProduct.fmpp.scala | 0 .../{scala => scala-2}/io/sphere/json/generic/JSONMacros.scala | 0 .../{scala => scala-2}/io/sphere/json/generic/package.fmpp.scala | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename json/json-derivation/src/main/{scala => scala-2}/io/sphere/json/ToJSONProduct.fmpp.scala (100%) rename json/json-derivation/src/main/{scala => scala-2}/io/sphere/json/generic/JSONMacros.scala (100%) rename json/json-derivation/src/main/{scala => scala-2}/io/sphere/json/generic/package.fmpp.scala (100%) diff --git a/json/json-derivation/src/main/scala/io/sphere/json/ToJSONProduct.fmpp.scala b/json/json-derivation/src/main/scala-2/io/sphere/json/ToJSONProduct.fmpp.scala similarity index 100% rename from json/json-derivation/src/main/scala/io/sphere/json/ToJSONProduct.fmpp.scala rename to json/json-derivation/src/main/scala-2/io/sphere/json/ToJSONProduct.fmpp.scala diff --git a/json/json-derivation/src/main/scala/io/sphere/json/generic/JSONMacros.scala b/json/json-derivation/src/main/scala-2/io/sphere/json/generic/JSONMacros.scala similarity index 100% rename from json/json-derivation/src/main/scala/io/sphere/json/generic/JSONMacros.scala rename to json/json-derivation/src/main/scala-2/io/sphere/json/generic/JSONMacros.scala diff --git a/json/json-derivation/src/main/scala/io/sphere/json/generic/package.fmpp.scala b/json/json-derivation/src/main/scala-2/io/sphere/json/generic/package.fmpp.scala similarity index 100% rename from json/json-derivation/src/main/scala/io/sphere/json/generic/package.fmpp.scala rename to json/json-derivation/src/main/scala-2/io/sphere/json/generic/package.fmpp.scala From 04916184da194fe621f2f8ac57445c9398c3bb40 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 8 May 2025 12:10:29 +0200 Subject: [PATCH 113/142] formatting --- .../scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala b/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala index d377f50e..24208d9a 100644 --- a/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala +++ b/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/generic/SumTypesDerivingSpec.scala @@ -3,7 +3,7 @@ package io.sphere.mongo.generic import com.mongodb.DBObject import io.sphere.mongo.MongoUtils.dbObj import io.sphere.mongo.generic.deriveMongoFormat -import io.sphere.mongo.format.{MongoFormat} +import io.sphere.mongo.format.MongoFormat import io.sphere.mongo.format.DefaultMongoFormats.given import org.bson.BSONObject import org.scalatest.Assertion From f8ea6ca2ea7c4e7cb85b89a277bea474f5a728fd Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 8 May 2025 13:38:23 +0200 Subject: [PATCH 114/142] Fix DeriveSingleton import issue --- .../generic/DeriveSingleton.scala | 44 +++-- .../sphere/json/DeriveSingletonJSONSpec.scala | 171 ------------------ .../sphere/json/DeriveSingletonJSONSpec.scala | 23 ++- 3 files changed, 47 insertions(+), 191 deletions(-) delete mode 100644 json/json-derivation/src/test/scala-3/io/sphere/json/DeriveSingletonJSONSpec.scala rename json/json-derivation/src/test/{scala-2 => scala}/io/sphere/json/DeriveSingletonJSONSpec.scala (85%) diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala index c51a6557..2a017212 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala @@ -6,43 +6,55 @@ import org.json4s.{JNull, JString, JValue} import scala.deriving.Mirror -inline def deriveSingletonJSON[A](using Mirror.Of[A]): JSON[A] = DeriveSingleton.derived +inline def deriveSingletonJSON[A](using Mirror.Of[A]): DeriveSingleton[A] = DeriveSingleton.derived -object DeriveSingleton { +// This is required so we don't summon normal JSON instances (maybe there's a better way to work around this) +trait DeriveSingleton[A] extends JSON[A] - inline given derived[A](using Mirror.Of[A]): JSON[A] = Derivation.derived[A] +object DeriveSingleton { + def instance[A]( + readFn: JValue => JValidation[A], + writeFn: A => JValue, + fieldSet: Set[String] = Set.empty): DeriveSingleton[A] = new { + + override def read(jval: JValue): JValidation[A] = readFn(jval) + override def write(value: A): JValue = writeFn(value) + override val fields: Set[String] = fieldSet + } + inline given derived[A](using Mirror.Of[A]): DeriveSingleton[A] = Derivation.derived[A] private object Derivation { import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} - inline def derived[A](using m: Mirror.Of[A]): JSON[A] = + inline def derived[A](using m: Mirror.Of[A]): DeriveSingleton[A] = inline m match { case s: Mirror.SumOf[A] => deriveTrait(s) case p: Mirror.ProductOf[A] => deriveObject(p) } - inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): JSON[A] = { + inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): DeriveSingleton[A] = { val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] val typeHintMap = traitMetaData.subTypeFieldRenames val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) - val jsons: Seq[JSON[Any]] = summonFormatters[mirrorOfSum.MirroredElemTypes] + val jsons: Seq[DeriveSingleton[Any]] = summonFormatters[mirrorOfSum.MirroredElemTypes] val names: Seq[String] = constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector .asInstanceOf[Vector[String]] - val jsonsByNames: Map[String, JSON[Any]] = names.zip(jsons).toMap + val jsonsByNames: Map[String, DeriveSingleton[Any]] = names.zip(jsons).toMap - JSON.instance( + DeriveSingleton.instance( readFn = { case JString(typeName) => val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) jsonsByNames.get(originalTypeName) match { case Some(json) => - json.read(JNull).map(_.asInstanceOf[A]) + val dummyValue = JNull + json.read(dummyValue).map(_.asInstanceOf[A]) case None => Validated.invalidNel(JSONParseError(s"'$typeName' is not a valid value")) } @@ -58,23 +70,23 @@ object DeriveSingleton { ) } - inline private def deriveObject[A](mirrorOfProduct: Mirror.ProductOf[A]): JSON[A] = - JSON.instance( + inline private def deriveObject[A](mirrorOfProduct: Mirror.ProductOf[A]): DeriveSingleton[A] = + DeriveSingleton.instance( writeFn = { _ => ??? }, // This is already taken care of in `deriveTrait` readFn = { _ => // Just create the object instance, no need to do anything else - val tuple = Tuple.fromArray(Array.empty[Any]) - val obj = mirrorOfProduct.fromTuple(tuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) + val obj = + mirrorOfProduct.fromTuple(EmptyTuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) Validated.Valid(obj) } ) - inline private def summonFormatters[T <: Tuple]: Vector[JSON[Any]] = + inline private def summonFormatters[T <: Tuple]: Vector[DeriveSingleton[Any]] = inline erasedValue[T] match { case _: EmptyTuple => Vector.empty case _: (t *: ts) => - summonInline[JSON[t]] - .asInstanceOf[JSON[Any]] +: summonFormatters[ts] + summonInline[DeriveSingleton[t]] + .asInstanceOf[DeriveSingleton[Any]] +: summonFormatters[ts] } } } diff --git a/json/json-derivation/src/test/scala-3/io/sphere/json/DeriveSingletonJSONSpec.scala b/json/json-derivation/src/test/scala-3/io/sphere/json/DeriveSingletonJSONSpec.scala deleted file mode 100644 index 4df91ae2..00000000 --- a/json/json-derivation/src/test/scala-3/io/sphere/json/DeriveSingletonJSONSpec.scala +++ /dev/null @@ -1,171 +0,0 @@ -package io.sphere.json - -import cats.data.Validated.Valid -import io.sphere.json.generic.* -import org.json4s.DefaultJsonFormats.given -import org.json4s.{DynamicJValueImplicits, JArray, JObject, JValue} -import org.json4s.JsonAST.{JField, JNothing} -import org.json4s.jackson.JsonMethods.{compact, render} -import org.scalatest.matchers.must.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class DeriveSingletonJSONSpec extends AnyWordSpec with Matchers { - "DeriveSingletonJSON" must { - "read normal singleton values" in { - val user = getFromJSON[UserWithPicture](""" - { - "userId": "foo-123", - "pictureSize": "Medium", - "pictureUrl": "http://exmple.com" - } - """) - - user must be(UserWithPicture("foo-123", Medium, "http://exmple.com")) - } - - "fail to read if singleton value is unknown" in { - a[JSONException] must be thrownBy getFromJSON[UserWithPicture](""" - { - "userId": "foo-123", - "pictureSize": "foo", - "pictureUrl": "http://exmple.com" - } - """) - } - - "write normal singleton values" in { - val userJson = toJValue(UserWithPicture("foo-123", Medium, "http://exmple.com")) - - val Valid(expectedJson) = parseJSON(""" - { - "userId": "foo-123", - "pictureSize": "Medium", - "pictureUrl": "http://exmple.com" - } - """): @unchecked - - filter(userJson) must be(expectedJson) - } - - "read custom singleton values" in { - val user = getFromJSON[UserWithPicture](""" - { - "userId": "foo-123", - "pictureSize": "bar", - "pictureUrl": "http://exmple.com" - } - """) - - user must be(UserWithPicture("foo-123", Custom, "http://exmple.com")) - } - - "write custom singleton values" in { - val userJson = toJValue(UserWithPicture("foo-123", Custom, "http://exmple.com")) - - val Valid(expectedJson) = parseJSON(""" - { - "userId": "foo-123", - "pictureSize": "bar", - "pictureUrl": "http://exmple.com" - } - """): @unchecked - - filter(userJson) must be(expectedJson) - } - - "write and consequently read, which must produce the original value" in { - val originalUser = UserWithPicture("foo-123", Medium, "http://exmple.com") - val newUser = getFromJSON[UserWithPicture](compact(render(toJValue(originalUser)))) - - newUser must be(originalUser) - } - - "read and write sealed trait with only one subtype" in { - val json = """ - { - "userId": "foo-123", - "pictureSize": "Medium", - "pictureUrl": "http://example.com", - "access": { - "type": "Authorized", - "project": "internal" - } - } - """ - val user = getFromJSON[UserWithPicture](json) - - user must be( - UserWithPicture( - "foo-123", - Medium, - "http://example.com", - Some(Access.Authorized("internal")))) - - val newJson = toJValue[UserWithPicture](user) - Valid(newJson) must be(parseJSON(json)) - - val Valid(newUser) = fromJValue[UserWithPicture](newJson): @unchecked - newUser must be(user) - } - } - - private def filter(jvalue: JValue): JValue = - jvalue.removeField { - case (_, JNothing) => true - case _ => false - } - - extension (jv: JValue) { - def removeField(p: JField => Boolean): JValue = jv.transform { case JObject(l) => - JObject(l.filterNot(p)) - } - - def transform(f: PartialFunction[JValue, JValue]): JValue = map { x => - f.applyOrElse[JValue, JValue](x, _ => x) - } - - def map(f: JValue => JValue): JValue = { - def rec(v: JValue): JValue = v match { - case JObject(l) => f(JObject(l.map { case (n, va) => (n, rec(va)) })) - case JArray(l) => f(JArray(l.map(rec))) - case x => f(x) - } - - rec(jv) - } - } -} - -sealed abstract class PictureSize(val weight: Int, val height: Int) - -case object Small extends PictureSize(100, 100) -case object Medium extends PictureSize(500, 450) -case object Big extends PictureSize(1024, 2048) - -@JSONTypeHint("bar") -case object Custom extends PictureSize(1, 2) - -object PictureSize { - // TODO We have to get rid of this - import DeriveSingleton.derived - - given JSON[PictureSize] = deriveSingletonJSON -} - -sealed trait Access -object Access { - // only one sub-type - case class Authorized(project: String) extends Access - - given JSON[Access] = deriveJSON -} - -case class UserWithPicture( - userId: String, - pictureSize: PictureSize, - pictureUrl: String, - access: Option[Access] = None) - -object UserWithPicture { - given JSON[UserWithPicture] = deriveJSON[UserWithPicture] -} diff --git a/json/json-derivation/src/test/scala-2/io/sphere/json/DeriveSingletonJSONSpec.scala b/json/json-derivation/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala similarity index 85% rename from json/json-derivation/src/test/scala-2/io/sphere/json/DeriveSingletonJSONSpec.scala rename to json/json-derivation/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala index 8f244864..369f6515 100644 --- a/json/json-derivation/src/test/scala-2/io/sphere/json/DeriveSingletonJSONSpec.scala +++ b/json/json-derivation/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala @@ -1,5 +1,6 @@ package io.sphere.json +import cats.data.Validated import cats.data.Validated.Valid import io.sphere.json.generic._ import org.json4s.JValue @@ -35,7 +36,7 @@ class DeriveSingletonJSONSpec extends AnyWordSpec with Matchers { "write normal singleton values" in { val userJson = toJValue(UserWithPicture("foo-123", Medium, "http://exmple.com")) - val Valid(expectedJson) = parseJSON(""" + val expectedJson = parseValidJSON(""" { "userId": "foo-123", "pictureSize": "Medium", @@ -61,7 +62,7 @@ class DeriveSingletonJSONSpec extends AnyWordSpec with Matchers { "write custom singleton values" in { val userJson = toJValue(UserWithPicture("foo-123", Custom, "http://exmple.com")) - val Valid(expectedJson) = parseJSON(""" + val expectedJson = parseValidJSON(""" { "userId": "foo-123", "pictureSize": "bar", @@ -103,16 +104,30 @@ class DeriveSingletonJSONSpec extends AnyWordSpec with Matchers { val newJson = toJValue[UserWithPicture](user) Valid(newJson) must be(parseJSON(json)) - val Valid(newUser) = fromJValue[UserWithPicture](newJson) + val newUser = fromValidJValue[UserWithPicture](newJson) newUser must be(user) } } - private def filter(jvalue: JValue): JValue = + private def filter(jvalue: JValue): JValue = { + import org.json4s.jvalue2monadic jvalue.removeField { case (_, JNothing) => true case _ => false } + } + + def parseValidJSON(string: String): JValue = + parseJSON(string) match { + case Valid(a) => a + case Validated.Invalid(e) => throw new Exception(e.head.toString) + } + + def fromValidJValue[A](jval: JValue)(implicit json: FromJSON[A]): A = + json.read(jval) match { + case Valid(a) => a + case Validated.Invalid(e) => throw new Exception(e.head.toString) + } } sealed abstract class PictureSize(val weight: Int, val height: Int) From 033873151d225d99c5240f9abe73665ff3bac404 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 8 May 2025 13:56:04 +0200 Subject: [PATCH 115/142] formatting --- .../scala-3/io.sphere.json/generic/DeriveSingleton.scala | 2 +- .../io/sphere/json/generic/JsonTypeSwitchSpec.scala | 7 ------- .../scala/io/sphere/json/DeriveSingletonJSONSpec.scala | 2 +- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala index 2a017212..1f8f4562 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala @@ -8,7 +8,7 @@ import scala.deriving.Mirror inline def deriveSingletonJSON[A](using Mirror.Of[A]): DeriveSingleton[A] = DeriveSingleton.derived -// This is required so we don't summon normal JSON instances (maybe there's a better way to work around this) +// This is required so we don't summon normal JSON instances in summonFormatters (maybe there's a better way to work around this) trait DeriveSingleton[A] extends JSON[A] object DeriveSingleton { diff --git a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala index 7f0fe943..ef6a16e6 100644 --- a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala +++ b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala @@ -100,7 +100,6 @@ object JsonTypeSwitchSpec { @JSONTypeHint("D2") case class D(int: Int) extends A trait Message - object Message { // this can be dangerous is the same class name is used in both sum types // ex if we define TypeA.Class1 && TypeB.Class1 @@ -109,22 +108,16 @@ object JsonTypeSwitchSpec { } sealed trait TypeA extends Message - object TypeA { case class ClassA1(number: Int) extends TypeA - case class ClassA2(name: String) extends TypeA - implicit val json: JSON[TypeA] = deriveJSON[TypeA] } sealed trait TypeB extends Message - object TypeB { case class ClassB1(valid: Boolean) extends TypeB - case class ClassB2(references: Seq[String]) extends TypeB - implicit val json: JSON[TypeB] = deriveJSON[TypeB] } } diff --git a/json/json-derivation/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala b/json/json-derivation/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala index 369f6515..da79c37a 100644 --- a/json/json-derivation/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala +++ b/json/json-derivation/src/test/scala/io/sphere/json/DeriveSingletonJSONSpec.scala @@ -126,7 +126,7 @@ class DeriveSingletonJSONSpec extends AnyWordSpec with Matchers { def fromValidJValue[A](jval: JValue)(implicit json: FromJSON[A]): A = json.read(jval) match { case Valid(a) => a - case Validated.Invalid(e) => throw new Exception(e.head.toString) + case Validated.Invalid(e) => throw new Exception(e.head.toString) } } From da44d9a3042f73334d4df2756ce5feb4016bdf8b Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 8 May 2025 14:37:24 +0200 Subject: [PATCH 116/142] Decrease differences between JSONSpec scala2/scala3 --- .../scala-2/io/sphere/json/JSONSpec.scala | 74 +++++++++------ .../io/sphere/json/generic/JSONSpec.scala | 95 ++++++++----------- .../io/sphere/mongo/SerializationTest.scala | 6 +- 3 files changed, 89 insertions(+), 86 deletions(-) diff --git a/json/json-derivation/src/test/scala-2/io/sphere/json/JSONSpec.scala b/json/json-derivation/src/test/scala-2/io/sphere/json/JSONSpec.scala index 325000d9..60832c5a 100644 --- a/json/json-derivation/src/test/scala-2/io/sphere/json/JSONSpec.scala +++ b/json/json-derivation/src/test/scala-2/io/sphere/json/JSONSpec.scala @@ -12,6 +12,8 @@ import org.scalatest.matchers.must.Matchers import org.scalatest.funspec.AnyFunSpec object JSONSpec { + case class Test(a: String) + case class Project(nr: Int, name: String, version: Int = 1, milestones: List[Milestone] = Nil) case class Milestone(name: String, date: Option[DateTime] = None) @@ -24,7 +26,7 @@ object JSONSpec { case class GenericA[A](a: A) extends GenericBase[A] case class GenericB[A](a: A) extends GenericBase[A] - object Singleton + case object Singleton sealed abstract class SingletonEnum case object SingletonA extends SingletonEnum @@ -45,6 +47,23 @@ object JSONSpec { class JSONSpec extends AnyFunSpec with Matchers { import JSONSpec._ + describe("JSON.apply") { + it("must find possible JSON instance") { + implicit val testJson: JSON[Test] = new JSON[Test] { + override def read(jval: JValue): JValidation[Test] = ??? + override def write(value: Test): JValue = ??? + } + + JSON[Test] must be(testJson) + } + + it("must create instance from FromJSON and ToJSON") { + JSON[Int] + JSON[List[Double]] + JSON[Map[String, Int]] + } + } + describe("JSON") { it("must read/write a custom class using custom typeclass instances") { import JSONSpec.{Milestone, Project} @@ -56,7 +75,7 @@ class JSONSpec extends AnyFunSpec with Matchers { ) def read(j: JValue): ValidatedNel[JSONError, Milestone] = j match { case o: JObject => - (field[String]("name")(o), field[Option[DateTime]]("date")(o)).mapN(Milestone) + (field[String]("name")(o), field[Option[DateTime]]("date")(o)).mapN(Milestone.apply) case _ => fail("JSON object expected.") } } @@ -73,7 +92,7 @@ class JSONSpec extends AnyFunSpec with Matchers { field[Int]("nr")(o), field[String]("name")(o), field[Int]("version", Some(1))(o), - field[List[Milestone]]("milestones", Some(Nil))(o)).mapN(Project) + field[List[Milestone]]("milestones", Some(Nil))(o)).mapN(Project.apply) case _ => fail("JSON object expected.") } } @@ -90,7 +109,7 @@ class JSONSpec extends AnyFunSpec with Matchers { "milestones":[{"name":"Bravo", "date": "xxx"}] } """ - val Invalid(errs) = fromJSON[Project](wrongTypeJSON) + val Invalid(errs) = fromJSON[Project](wrongTypeJSON): @unchecked errs.toList must equal( List( JSONFieldError(List("nr"), "JSON Number in the range of an Int expected."), @@ -110,75 +129,77 @@ class JSONSpec extends AnyFunSpec with Matchers { it("must provide derived JSON instances for product types (case classes)") { import JSONSpec.{Milestone, Project} - implicit val milestoneJSON = deriveJSON[Milestone] - implicit val projectJSON = deriveJSON[Project] + implicit val milestoneJSON: JSON[Milestone] = deriveJSON[Milestone] + implicit val projectJSON: JSON[Project] = deriveJSON[Project] val proj = Project(42, "Linux", 7, Milestone("1.0") :: Milestone("2.0") :: Milestone("3.0") :: Nil) fromJSON[Project](toJSON(proj)) must equal(Valid(proj)) } it("must handle empty String") { - val Invalid(err) = fromJSON[Int]("") + val Invalid(err) = fromJSON[Int](""): @unchecked err.toList.head mustBe a[JSONParseError] } it("must provide user-friendly error by empty String") { - val Invalid(err) = fromJSON[Int]("") + val Invalid(err) = fromJSON[Int](""): @unchecked err.toList mustEqual List(JSONParseError("No content to map due to end-of-input")) } it("must handle incorrect json") { - val Invalid(err) = fromJSON[Int]("""{"key: "value"}""") + val Invalid(err) = fromJSON[Int]("""{"key: "value"}"""): @unchecked err.toList.head mustBe a[JSONParseError] } it("must provide user-friendly error by incorrect json") { - val Invalid(err) = fromJSON[Int]("""{"key: "value"}""") + val Invalid(err) = fromJSON[Int]("""{"key: "value"}"""): @unchecked err.toList mustEqual List(JSONParseError( "Unexpected character ('v' (code 118)): was expecting a colon to separate field name and value")) } it("must provide derived JSON instances for sum types") { - implicit val animalJSON = deriveJSON[Animal] - List(Bird("Peewee"), Dog("Hasso"), Cat("Felidae")).foreach { a: Animal => + implicit val animalJSON: JSON[Animal] = deriveJSON + List(Bird("Peewee"), Dog("Hasso"), Cat("Felidae")).foreach { (a: Animal) => fromJSON[Animal](toJSON(a)) must equal(Valid(a)) } } it("must provide derived instances for product types with concrete type parameters") { - implicit val aJSON = deriveJSON[GenericA[String]] + implicit val aJSON: JSON[GenericA[String]] = deriveJSON[GenericA[String]] val a = GenericA("hello") fromJSON[GenericA[String]](toJSON(a)) must equal(Valid(a)) } it("must provide derived instances for product types with generic type parameters") { - implicit def aJSON[A: FromJSON: ToJSON] = deriveJSON[GenericA[A]] + implicit def aJSON[A: FromJSON: ToJSON]: JSON[GenericA[A]] = deriveJSON[GenericA[A]] val a = GenericA("hello") fromJSON[GenericA[String]](toJSON(a)) must equal(Valid(a)) } it("must provide derived instances for singleton objects") { - implicit val singletonJSON = deriveJSON[Singleton.type] + implicit val singletonJSON: JSON[JSONSpec.Singleton.type] = + deriveJSON[JSONSpec.Singleton.type] + val json = s"""[${toJSON(Singleton)}]""" withClue(json) { fromJSON[Seq[Singleton.type]](json) must equal(Valid(Seq(Singleton))) } - implicit val singleEnumJSON = deriveJSON[SingletonEnum] - List(SingletonA, SingletonB, SingletonC).foreach { s: SingletonEnum => + implicit val singleEnumJSON: JSON[SingletonEnum] = deriveJSON[SingletonEnum] + List(SingletonA, SingletonB, SingletonC).foreach { (s: SingletonEnum) => fromJSON[SingletonEnum](toJSON(s)) must equal(Valid(s)) } } it("must provide derived instances for sum types with a mix of case class / object") { - implicit val mixedJSON = deriveJSON[Mixed] - List(SingletonMixed, RecordMixed(1)).foreach { m: Mixed => + implicit val mixedJSON: JSON[Mixed] = deriveJSON + List(SingletonMixed, RecordMixed(1)).foreach { (m: Mixed) => fromJSON[Mixed](toJSON(m)) must equal(Valid(m)) } } it("must provide derived instances for scala.Enumeration") { - implicit val scalaEnumJSON = deriveJSON[ScalaEnum.Value] + implicit val scalaEnumJSON: JSON[ScalaEnum.Value] = jsonEnum(ScalaEnum) ScalaEnum.values.foreach { v => val json = s"""[${toJSON(v)}]""" withClue(json) { @@ -188,7 +209,7 @@ class JSONSpec extends AnyFunSpec with Matchers { } it("must handle subclasses correctly in `jsonTypeSwitch`") { - implicit val jsonImpl = TestSubjectBase.json + implicit val jsonImpl: JSON[TestSubjectBase] = TestSubjectBase.json val testSubjects = List[TestSubjectBase]( TestSubjectConcrete1("testSubject1"), @@ -203,9 +224,7 @@ class JSONSpec extends AnyFunSpec with Matchers { fromJSON[TestSubjectBase](json) must equal(Valid(testSubject)) } } - } - } describe("ToJSON and FromJSON") { @@ -278,8 +297,8 @@ class JSONSpec extends AnyFunSpec with Matchers { } it("must provide derived instances for scala.Enumeration") { - implicit val toScalaEnumJSON = toJsonEnum(ScalaEnum) - implicit val fromScalaEnumJSON = fromJsonEnum(ScalaEnum) + implicit val toScalaEnumJSON: ToJSON[JSONSpec.ScalaEnum.Value] = toJsonEnum(ScalaEnum) + implicit val fromScalaEnumJSON: FromJSON[JSONSpec.ScalaEnum.Value] = fromJsonEnum(ScalaEnum) ScalaEnum.values.foreach { v => val json = s"""[${toJSON(v)}]""" withClue(json) { @@ -355,6 +374,7 @@ case class TestSubjectConcrete1(c1: String) extends TestSubjectCategoryA case class TestSubjectConcrete2(c2: String) extends TestSubjectCategoryA case class TestSubjectConcrete3(c3: String) extends TestSubjectCategoryB +@JSONTypeHint("foo2") case class TestSubjectConcrete4(c4: String) extends TestSubjectCategoryB object TestSubjectCategoryA { @@ -367,8 +387,8 @@ object TestSubjectCategoryB { object TestSubjectBase { val json: JSON[TestSubjectBase] = { - implicit val jsonA = TestSubjectCategoryA.json - implicit val jsonB = TestSubjectCategoryB.json + implicit val jsonA: JSON[TestSubjectCategoryA] = TestSubjectCategoryA.json + implicit val jsonB: JSON[TestSubjectCategoryB] = TestSubjectCategoryB.json jsonTypeSwitch[TestSubjectBase, TestSubjectCategoryA, TestSubjectCategoryB](Nil) } diff --git a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala index 211016b3..3df510db 100644 --- a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala +++ b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala @@ -43,8 +43,7 @@ object JSONSpec { val One, Two, Three = Value } - // JSON instances for recursive data types cannot be derived - case class Node(value: Option[List[Node]]) + // case class Node(value: Option[List[Node]]) // JSON instances for recursive data types cannot be derived } class JSONSpec extends AnyFunSpec with Matchers { @@ -76,7 +75,6 @@ class JSONSpec extends AnyFunSpec with Matchers { JField("name", JString(m.name)) :: JField("date", toJValue(m.date)) :: Nil ) - def read(j: JValue): ValidatedNel[JSONError, Milestone] = j match { case o: JObject => (field[String]("name")(o), field[Option[DateTime]]("date")(o)).mapN(Milestone.apply) @@ -88,10 +86,8 @@ class JSONSpec extends AnyFunSpec with Matchers { JField("nr", JInt(p.nr)) :: JField("name", JString(p.name)) :: JField("version", JInt(p.version)) :: - JField("milestones", toJValue(p.milestones)) :: - Nil + JField("milestones", toJValue(p.milestones)) :: Nil ) - def read(jval: JValue): ValidatedNel[JSONError, Project] = jval match { case o: JObject => ( @@ -107,8 +103,7 @@ class JSONSpec extends AnyFunSpec with Matchers { fromJSON[Project](toJSON(proj)) must equal(Valid(proj)) // Now some invalid JSON to test the error accumulation - val wrongTypeJSON = - """ + val wrongTypeJSON = """ { "nr":"1", "name":23, @@ -116,7 +111,6 @@ class JSONSpec extends AnyFunSpec with Matchers { "milestones":[{"name":"Bravo", "date": "xxx"}] } """ - val Invalid(errs) = fromJSON[Project](wrongTypeJSON): @unchecked errs.toList must equal( List( @@ -137,8 +131,8 @@ class JSONSpec extends AnyFunSpec with Matchers { it("must provide derived JSON instances for product types (case classes)") { import JSONSpec.{Milestone, Project} - given JSON[Milestone] = deriveJSON[Milestone] - given JSON[Project] = deriveJSON[Project] + implicit val milestoneJSON: JSON[Milestone] = deriveJSON[Milestone] + implicit val projectJSON: JSON[Project] = deriveJSON[Project] val proj = Project(42, "Linux", 7, Milestone("1.0") :: Milestone("2.0") :: Milestone("3.0") :: Nil) fromJSON[Project](toJSON(proj)) must equal(Valid(proj)) @@ -166,20 +160,20 @@ class JSONSpec extends AnyFunSpec with Matchers { } it("must provide derived JSON instances for sum types") { - given JSON[Animal] = deriveJSON - List(Bird("Peewee"), Dog("Hasso"), Cat("Felidae")).foreach { animal => - fromJSON[Animal](toJSON(animal)) must equal(Valid(animal)) + implicit val animalJSON: JSON[Animal] = deriveJSON + List(Bird("Peewee"), Dog("Hasso"), Cat("Felidae")).foreach { (a: Animal) => + fromJSON[Animal](toJSON(a)) must equal(Valid(a)) } } it("must provide derived instances for product types with concrete type parameters") { - given JSON[GenericA[String]] = deriveJSON[GenericA[String]] + implicit val aJSON: JSON[GenericA[String]] = deriveJSON[GenericA[String]] val a = GenericA("hello") fromJSON[GenericA[String]](toJSON(a)) must equal(Valid(a)) } it("must provide derived instances for product types with generic type parameters") { - implicit def aJSON[A: JSON]: JSON[GenericA[A]] = deriveJSON[GenericA[A]] + implicit def aJSON[A: FromJSON: ToJSON]: JSON[GenericA[A]] = deriveJSON[GenericA[A]] val a = GenericA("hello") fromJSON[GenericA[String]](toJSON(a)) must equal(Valid(a)) } @@ -194,20 +188,20 @@ class JSONSpec extends AnyFunSpec with Matchers { } implicit val singleEnumJSON: JSON[SingletonEnum] = deriveJSON[SingletonEnum] - List(SingletonA, SingletonB, SingletonC).foreach { s => + List(SingletonA, SingletonB, SingletonC).foreach { (s: SingletonEnum) => fromJSON[SingletonEnum](toJSON(s)) must equal(Valid(s)) } } it("must provide derived instances for sum types with a mix of case class / object") { - given JSON[Mixed] = deriveJSON - List(SingletonMixed, RecordMixed(1)).foreach { m => + implicit val mixedJSON: JSON[Mixed] = deriveJSON + List(SingletonMixed, RecordMixed(1)).foreach { (m: Mixed) => fromJSON[Mixed](toJSON(m)) must equal(Valid(m)) } } - it("must provide instances for scala.Enumeration") { - // We dropped support for deriveJSON, because there was no derivation anyway, the derivation just called these methods - implicit val scalaEnumJSON: JSON[JSONSpec.ScalaEnum.Value] = jsonEnum(ScalaEnum) + + it("must provide derived instances for scala.Enumeration") { + implicit val scalaEnumJSON: JSON[ScalaEnum.Value] = jsonEnum(ScalaEnum) ScalaEnum.values.foreach { v => val json = s"""[${toJSON(v)}]""" withClue(json) { @@ -217,7 +211,7 @@ class JSONSpec extends AnyFunSpec with Matchers { } it("must handle subclasses correctly in `jsonTypeSwitch`") { - given JSON[TestSubjectBase] = TestSubjectBase.json + implicit val jsonImpl: JSON[TestSubjectBase] = TestSubjectBase.json val testSubjects = List[TestSubjectBase]( TestSubjectConcrete1("testSubject1"), @@ -236,33 +230,6 @@ class JSONSpec extends AnyFunSpec with Matchers { } describe("ToJSON and FromJSON") { - - it("must provide derived JSON instances for product types (case classes)") { - import JSONSpec.{Milestone, Project} - given ToJSON[Milestone] = ToJSON.derived[Milestone] - - given ToJSON[Project] = ToJSON.derived[Project] - - given FromJSON[Milestone] = FromJSON.derived[Milestone] - - given FromJSON[Project] = FromJSON.derived[Project] - - val proj = - Project(42, "Linux", 7, Milestone("1.0") :: Milestone("2.0") :: Milestone("3.0") :: Nil) - fromJSON[Project](toJSON(proj)) must equal(Valid(proj)) - } - - it("must provide instances for scala.Enumeration") { - implicit val toScalaEnumJSON = toJsonEnum(ScalaEnum) - implicit val fromScalaEnumJSON = fromJsonEnum(ScalaEnum) - ScalaEnum.values.foreach { v => - val json = s"""[${toJSON(v)}]""" - withClue(json) { - fromJSON[Seq[ScalaEnum.Value]](json) must equal(Valid(Seq(v))) - } - } - } - it("must provide derived JSON instances for sum types") { // ToJSON given ToJSON[Bird] = ToJSON.derived[Bird] @@ -302,6 +269,17 @@ class JSONSpec extends AnyFunSpec with Matchers { } } + it("must provide derived instances for scala.Enumeration") { + implicit val toScalaEnumJSON: ToJSON[JSONSpec.ScalaEnum.Value] = toJsonEnum(ScalaEnum) + implicit val fromScalaEnumJSON: FromJSON[JSONSpec.ScalaEnum.Value] = fromJsonEnum(ScalaEnum) + ScalaEnum.values.foreach { v => + val json = s"""[${toJSON(v)}]""" + withClue(json) { + fromJSON[Seq[ScalaEnum.Value]](json) must equal(Valid(Seq(v))) + } + } + } + it("must handle subclasses correctly in `jsonTypeSwitch`") { // ToJSON given ToJSON[TestSubjectConcrete1] = ToJSON.derived @@ -343,6 +321,17 @@ class JSONSpec extends AnyFunSpec with Matchers { } + it("must provide derived JSON instances for product types (case classes)") { + import JSONSpec.{Milestone, Project} + given ToJSON[Milestone] = ToJSON.derived[Milestone] + given ToJSON[Project] = ToJSON.derived[Project] + given FromJSON[Milestone] = FromJSON.derived[Milestone] + given FromJSON[Project] = FromJSON.derived[Project] + + val proj = + Project(42, "Linux", 7, Milestone("1.0") :: Milestone("2.0") :: Milestone("3.0") :: Nil) + fromJSON[Project](toJSON(proj)) must equal(Valid(proj)) + } } } @@ -360,19 +349,17 @@ case class TestSubjectConcrete3(c3: String) extends TestSubjectCategoryB case class TestSubjectConcrete4(c4: String) extends TestSubjectCategoryB object TestSubjectCategoryA { - val json: JSON[TestSubjectCategoryA] = deriveJSON[TestSubjectCategoryA] } object TestSubjectCategoryB { - val json: JSON[TestSubjectCategoryB] = deriveJSON[TestSubjectCategoryB] } object TestSubjectBase { val json: JSON[TestSubjectBase] = { - given JSON[TestSubjectCategoryA] = TestSubjectCategoryA.json - given JSON[TestSubjectCategoryB] = TestSubjectCategoryB.json + implicit val jsonA: JSON[TestSubjectCategoryA] = TestSubjectCategoryA.json + implicit val jsonB: JSON[TestSubjectCategoryB] = TestSubjectCategoryB.json jsonTypeSwitch[TestSubjectBase, (TestSubjectCategoryA, TestSubjectCategoryB)]() } diff --git a/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/SerializationTest.scala b/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/SerializationTest.scala index ec281dba..9b610376 100644 --- a/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/SerializationTest.scala +++ b/mongo/mongo-derivation/src/test/scala-2/io/sphere/mongo/SerializationTest.scala @@ -29,11 +29,7 @@ class SerializationTest extends AnyWordSpec with Matchers { } "generate a format that serializes optional fields with value None as BSON objects without that field" in { - val testFormat: MongoFormat[Something] = - io.sphere.mongo.generic.mongoProduct[Something, Option[Int], Int] { - (a: Option[Int], b: Int) => Something(a, b) - } - + val testFormat: MongoFormat[Something] = io.sphere.mongo.generic.deriveMongoFormat val serializedObject = testFormat.toMongoValue(Something(None, 1)).asInstanceOf[DBObject] serializedObject.keySet().contains("b") must be(true) serializedObject.keySet().contains("a") must be(false) From 6c12f676cd6ded0ebd01a71ccb7d6959b755b34d Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Sat, 10 May 2025 11:09:15 +0200 Subject: [PATCH 117/142] Use typeSwitches for the trait case of derivations. --- .../generic/DeriveFromJSON.scala | 42 +------ .../io.sphere.json/generic/DeriveToJSON.scala | 40 ++---- .../generic/JSONTypeSwitch.scala | 4 +- .../io.sphere.json/generic/generic.scala | 4 +- .../io/sphere/json/generic/JSONSpec.scala | 2 +- .../json/generic/JsonTypeSwitchSpec.scala | 8 +- .../io/sphere/mongo/format/MongoFormat.scala | 115 +++++------------- .../mongo/generic/AnnotationReader.scala | 68 ++++++++--- .../io/sphere/mongo/generic/generic.scala | 49 +++++--- .../io/sphere/mongo/SerializationTest.scala | 27 ++-- .../mongo/generic/MongoTypeSwitchSpec.scala | 4 +- 11 files changed, 149 insertions(+), 214 deletions(-) diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala index 37343301..6107c22a 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala @@ -2,17 +2,11 @@ package io.sphere.json.generic import cats.data.Validated import cats.syntax.traverse.* -import io.sphere.json.field -import io.sphere.json.generic.{AnnotationReader, Field, TraitMetaData, TypeMetaData} +import io.sphere.json.* import org.json4s.JsonAST.* -import org.json4s.DefaultReaders.StringReader -import org.json4s.{jvalue2monadic, jvalue2readerSyntax} import scala.deriving.Mirror -import io.sphere.json.FromJSON -import io.sphere.json.* - trait DeriveFromJSON { inline given derived[A](using Mirror.Of[A]): FromJSON[A] = Derivation.derived[A] @@ -22,42 +16,14 @@ trait DeriveFromJSON { inline def derived[A](using m: Mirror.Of[A]): FromJSON[A] = inline m match { - case s: Mirror.SumOf[A] => deriveTrait(s) + case s: Mirror.SumOf[A] => fromJsonTypeSwitch[A, s.MirroredElemTypes] case p: Mirror.ProductOf[A] => deriveCaseClass(p) } - inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): FromJSON[A] = { - val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] - - val reverseTypeHintMap: Map[String, String] = - traitMetaData.subTypeFieldRenames.map((on, n) => (n, on)) - val fromJsons: Seq[FromJSON[Any]] = summonFromJsons[mirrorOfSum.MirroredElemTypes] - - val names: Seq[String] = - constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector - .asInstanceOf[Vector[String]] - - val jsonsByNames: Map[String, FromJSON[Any]] = names.zip(fromJsons).toMap - - FromJSON.instance( - readFn = { - case jObject: JObject => - val typeName = (jObject \ traitMetaData.typeDiscriminator).as[String] - val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - jsonsByNames(originalTypeName).read(jObject).map(_.asInstanceOf[A]) - - case x => - Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$x'")) - } - ) - } - inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): FromJSON[A] = { val caseClassMetaData: TypeMetaData = AnnotationReader.readTypeMetaData[A] - val fromJsons: Vector[FromJSON[Any]] = - summonFromJsons[mirrorOfProduct.MirroredElemTypes] - val fieldsAndJsons: Vector[(Field, FromJSON[Any])] = - caseClassMetaData.fields.zip(fromJsons) + val fromJsons: Vector[FromJSON[Any]] = summonFromJsons[mirrorOfProduct.MirroredElemTypes] + val fieldsAndJsons: Vector[(Field, FromJSON[Any])] = caseClassMetaData.fields.zip(fromJsons) val fieldNames: Vector[String] = fieldsAndJsons.flatMap { (field, fromJson) => if (field.embedded) fromJson.fields.toVector :+ field.name diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala index 194960d9..70b8f572 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala @@ -9,46 +9,16 @@ trait DeriveToJSON { inline given derived[A](using Mirror.Of[A]): ToJSON[A] = Derivation.derived[A] - private def addField(jObject: JObject, field: Field, jValue: JValue): JValue = - jValue match { - case o: JObject => - if (field.embedded) JObject(jObject.obj ++ o.obj) - else JObject(jObject.obj :+ (field.fieldName -> o)) - case other => JObject(jObject.obj :+ (field.fieldName -> other)) - } - protected object Derivation { import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} inline def derived[A](using m: Mirror.Of[A]): ToJSON[A] = inline m match { - case s: Mirror.SumOf[A] => deriveTrait(s) + case s: Mirror.SumOf[A] => toJsonTypeSwitch[A, s.MirroredElemTypes] case p: Mirror.ProductOf[A] => deriveCaseClass(p) } - inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): ToJSON[A] = { - val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] - - val jsons: Seq[ToJSON[Any]] = summonToJson[mirrorOfSum.MirroredElemTypes] - - val names: Seq[String] = - constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector - .asInstanceOf[Vector[String]] - - val jsonsByNames: Map[String, ToJSON[Any]] = names.zip(jsons).toMap - - ToJSON.instance { value => - // we never get a trait here, only classes, it's safe to assume Product - val originalTypeName = value.asInstanceOf[Product].productPrefix - val typeName = - traitMetaData.subTypeFieldRenames.getOrElse(originalTypeName, originalTypeName) - val json = jsonsByNames(originalTypeName).write(value).asInstanceOf[JObject] - val typeDiscriminator = traitMetaData.typeDiscriminator -> JString(typeName) - JObject(typeDiscriminator :: json.obj) - } - } - inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): ToJSON[A] = { val caseClassMetaData: TypeMetaData = AnnotationReader.readTypeMetaData[A] val toJsons: Vector[ToJSON[Any]] = summonToJson[mirrorOfProduct.MirroredElemTypes] @@ -73,4 +43,12 @@ trait DeriveToJSON { } } + private def addField(jObject: JObject, field: Field, jValue: JValue): JValue = + jValue match { + case o: JObject => + if (field.embedded) JObject(jObject.obj ++ o.obj) + else JObject(jObject.obj :+ (field.fieldName -> o)) + case other => JObject(jObject.obj :+ (field.fieldName -> other)) + } + } diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/JSONTypeSwitch.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/JSONTypeSwitch.scala index e89306b5..ea115e77 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/JSONTypeSwitch.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/JSONTypeSwitch.scala @@ -24,7 +24,7 @@ object JSONTypeSwitch { formattersAndMetaData.partitionMap { (meta, formatter) => if (meta.isTrait) { val formatterByName = meta.subtypes.map((fieldName, m) => m.name -> formatter) - Left((formatterByName, meta.subTypeFieldRenames)) + Left(formatterByName -> meta.subTypeFieldRenames) } else { Right(meta.top.name -> formatter) } @@ -77,7 +77,7 @@ object JSONTypeSwitch { } } - inline def jsonTypeSwitch[SuperType, SubTypes <: Tuple](): JSON[SuperType] = { + inline def jsonTypeSwitch[SuperType, SubTypes <: Tuple]: JSON[SuperType] = { val info = readTraitInformation[SuperType, SubTypes] val fromJson = fromJsonTypeSwitch[SuperType](info) val toJson = toJsonTypeSwitch[SuperType](info) diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala index 31d55e90..e691c2f7 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala @@ -15,8 +15,8 @@ inline def fromJsonEnum(e: Enumeration): FromJSON[e.Value] = EnumerationInstance // This can be used instead of deriveJSON inline def jsonEnum(e: Enumeration): JSON[e.Value] = EnumerationInstances.jsonEnum(e) -inline def jsonTypeSwitch[SuperType, SubTypes <: Tuple](): JSON[SuperType] = - JSONTypeSwitch.jsonTypeSwitch[SuperType, SubTypes]() +inline def jsonTypeSwitch[SuperType, SubTypes <: Tuple]: JSON[SuperType] = + JSONTypeSwitch.jsonTypeSwitch[SuperType, SubTypes] inline def toJsonTypeSwitch[SuperType, SubTypes <: Tuple]: ToJSON[SuperType] = { val info = JSONTypeSwitch.readTraitInformation[SuperType, SubTypes] diff --git a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala index 3df510db..236feb38 100644 --- a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala +++ b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala @@ -361,6 +361,6 @@ object TestSubjectBase { implicit val jsonA: JSON[TestSubjectCategoryA] = TestSubjectCategoryA.json implicit val jsonB: JSON[TestSubjectCategoryB] = TestSubjectCategoryB.json - jsonTypeSwitch[TestSubjectBase, (TestSubjectCategoryA, TestSubjectCategoryB)]() + jsonTypeSwitch[TestSubjectBase, (TestSubjectCategoryA, TestSubjectCategoryB)] } } diff --git a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala index ef6a16e6..a2b9b875 100644 --- a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala +++ b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala @@ -16,7 +16,7 @@ class JsonTypeSwitchSpec extends AnyWordSpec with Matchers { "derive a subset of a sealed trait" in { given JSON[B] = deriveJSON[B] - val format = jsonTypeSwitch[A, (B, C)]() + val format = jsonTypeSwitch[A, (B, C)] val b = B(123) val jsonB = format.write(b) @@ -34,7 +34,7 @@ class JsonTypeSwitchSpec extends AnyWordSpec with Matchers { } "derive a subset of a sealed trait with a mongoKey" in { - val format = jsonTypeSwitch[A, (B, D)]() + val format = jsonTypeSwitch[A, (B, D)] val d = D(123) val json = format.write(d) @@ -78,7 +78,7 @@ class JsonTypeSwitchSpec extends AnyWordSpec with Matchers { override def write(value: B): JValue = JObject(List("field" -> JString(s"Custom-B-${value.int}"))) } - val format = jsonTypeSwitch[A, (B, D, C)]() + val format = jsonTypeSwitch[A, (B, D, C)] List( D(2345), @@ -104,7 +104,7 @@ object JsonTypeSwitchSpec { // this can be dangerous is the same class name is used in both sum types // ex if we define TypeA.Class1 && TypeB.Class1 // as both will use the same type value discriminator - implicit val json: JSON[Message] = jsonTypeSwitch[Message, (TypeA, TypeB)]() + implicit val json: JSON[Message] = jsonTypeSwitch[Message, (TypeA, TypeB)] } sealed trait TypeA extends Message diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala index 10eedab9..fc2b9429 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala @@ -1,7 +1,7 @@ package io.sphere.mongo.format import com.mongodb.BasicDBObject -import io.sphere.mongo.generic.{AnnotationReader, Field} +import io.sphere.mongo.generic.{AnnotationReader, Field, mongoTypeSwitch} import io.sphere.util.VectorUtils.* import org.bson.BSONObject import org.bson.types.ObjectId @@ -29,10 +29,8 @@ trait MongoFormat[A] extends Serializable { */ trait TraitMongoFormat[A] extends MongoFormat[A] { // This approach is somewhat slow, the reason I chose to implement it like this is because: - // 1. We don't have nested trait structures anyway, the scala 2 version didn't even work for this case. - // So this is more of a proof of concept feature - // 2. I didn't find a way to check types runtime when you have a nested trait hierarchy, because of erasure. - // So this instance wouldn't know if it's really dealing with one of its subtypes or with another trait's subtype. + // 1. this approach supports different type discriminators for different traits + // 2. no need for classtag def attemptWrite(a: A): Try[Any] = Try(toMongoValue(a)) def attemptRead(bson: BSONObject): Try[A] = Try(fromMongoValue(bson)) @@ -61,70 +59,15 @@ object MongoFormat { override val fields: Set[String] = fieldSet } - private def addField(bson: BasicDBObject, field: Field, mongoType: Any): Unit = - if (!field.ignored) - mongoType match { - case s: SimpleMongoType => bson.put(field.name, s) - case innerBson: BasicDBObject => - if (field.embedded) innerBson.entrySet().forEach(p => bson.put(p.getKey, p.getValue)) - else bson.put(field.name, innerBson) - case MongoNothing => - } - private object Derivation { import scala.compiletime.{constValue, constValueTuple, erasedValue, error, summonInline} inline def derived[A](using m: Mirror.Of[A]): MongoFormat[A] = inline m match { - case s: Mirror.SumOf[A] => deriveTrait(s) + case s: Mirror.SumOf[A] => mongoTypeSwitch[A, s.MirroredElemTypes] case p: Mirror.ProductOf[A] => deriveCaseClass(p) } - inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): MongoFormat[A] = { - val traitMetaData = AnnotationReader.readTraitMetaData[A] - val typeHintMap = traitMetaData.subTypeTypeHints - val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) - val formatters = summonFormatters[mirrorOfSum.MirroredElemTypes] - val subTypeNames = constValueTuple[mirrorOfSum.MirroredElemLabels].productIterator.toVector - .asInstanceOf[Vector[String]] - val pairedFormatterWithSubtypeName = subTypeNames.zip(formatters) - val (caseClassFormatterList, traitFormatters) = pairedFormatterWithSubtypeName.partitionMap { - case kv @ (name, formatter) => - formatter match { - case traitFormatter: TraitMongoFormat[_] => Right(traitFormatter) - case _ => Left(kv) - } - } - val caseClassFormatters = caseClassFormatterList.toMap - - TraitMongoFormat.instance( - toMongo = { a => - traitFormatters.view.map(_.attemptWrite(a)).find(_.isSuccess).map(_.get) match { - case Some(bson) => bson - case None => - val originalTypeName = a.asInstanceOf[Product].productPrefix - val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) - val bson = - caseClassFormatters(originalTypeName).toMongoValue(a).asInstanceOf[BasicDBObject] - bson.put(traitMetaData.typeDiscriminator, typeName) - bson - } - }, - fromMongo = { - case bson: BasicDBObject => - traitFormatters.view.map(_.attemptRead(bson)).find(_.isSuccess).map(_.get) match { - case Some(a) => a.asInstanceOf[A] - case None => - val typeName = bson.get(traitMetaData.typeDiscriminator).asInstanceOf[String] - val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - caseClassFormatters(originalTypeName).fromMongoValue(bson).asInstanceOf[A] - } - case x => - throw new Exception(s"BsonObject is expected for a Trait subtype, instead got $x") - } - ) - } - inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): MongoFormat[A] = { val caseClassMetaData = AnnotationReader.readTypeMetaData[A] val formatters = summonFormatters[mirrorOfProduct.MirroredElemTypes] @@ -146,27 +89,8 @@ object MongoFormat { }, fromMongo = { case bson: BasicDBObject => - val fields = fieldsAndFormatters - .map { case (field, format) => - def defaultValue = field.defaultArgument.orElse(format.default) - - if (field.ignored) - defaultValue.getOrElse { - throw new Exception( - s"Ignored Mongo field '${field.name}' must have a default value.") - } - else if (field.embedded) format.fromMongoValue(bson) - else { - val value = bson.get(field.name) - if (value ne null) format.fromMongoValue(value.asInstanceOf[Any]) - else - defaultValue.getOrElse { - throw new Exception( - s"Missing required field '${field.name}' on deserialization.") - } - } - } - val tuple = Tuple.fromArray(fields.toArray) + val valuesOfClass = fieldsAndFormatters.map(readField(bson)) + val tuple = Tuple.fromArray(valuesOfClass.toArray) mirrorOfProduct.fromTuple(tuple.asInstanceOf[mirrorOfProduct.MirroredElemTypes]) case x => throw new Exception(s"BasicDBObject is expected for a class, instead got: $x") @@ -184,4 +108,31 @@ object MongoFormat { } } + + private def addField(bson: BasicDBObject, field: Field, mongoType: Any): Unit = + if (!field.ignored) + mongoType match { + case s: SimpleMongoType => bson.put(field.name, s) + case innerBson: BasicDBObject => + if (field.embedded) innerBson.entrySet().forEach(p => bson.put(p.getKey, p.getValue)) + else bson.put(field.name, innerBson) + case MongoNothing => + } + + private def readField(bson: BSONObject)(field: Field, format: MongoFormat[Any]): Any = { + def defaultValue = field.defaultArgument.orElse(format.default) + if (field.ignored) + defaultValue.getOrElse { + throw new Exception(s"Ignored Mongo field '${field.name}' must have a default value.") + } + else if (field.embedded) format.fromMongoValue(bson) + else { + val value = bson.get(field.name) + if (value ne null) format.fromMongoValue(value.asInstanceOf[Any]) + else + defaultValue.getOrElse { + throw new Exception(s"Missing required field '${field.name}' on deserialization.") + } + } + } } diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/AnnotationReader.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/AnnotationReader.scala index 94c48e87..e4aa1f4b 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/AnnotationReader.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/AnnotationReader.scala @@ -52,8 +52,54 @@ class AnnotationReader(using q: Quotes) { import q.reflect.* def readTypeMetaData[T: Type]: Expr[TypeMetaData] = { - val sym = TypeRepr.of[T].typeSymbol - typeMetaData(sym) + val tpe = TypeRepr.of[T] + val termSym = tpe.termSymbol + val typeSym = tpe.typeSymbol + if (termSym.flags.is(Flags.Enum) && typeSym.flags.is(Flags.Enum)) + typeMetaDataForEnumObjects(termSym) + else + typeMetaData(typeSym) + + } + + private def typeMetaDataForEnumObjects(sym: Symbol): Expr[TypeMetaData] = { + val name = Expr(sym.name) + val typeHint = sym.annotations.map(findTypeHint).find(_.isDefined).flatten match { + case Some(th) => '{ Some($th) } + case None => '{ None } + } + '{ + TypeMetaData( + name = $name, + typeHintRaw = $typeHint, + fields = Vector.empty + ) + } + } + + private def typeMetaData(sym: Symbol): Expr[TypeMetaData] = { + val caseParams = sym.primaryConstructor.paramSymss.take(1).flatten + val fields = Varargs(caseParams.zipWithIndex.map(collectFieldInfo(sym.companionModule))) + val name = + if (sym.flags.is(Flags.Enum)) { + Expr(sym.name) + } else if (sym.flags.is(Flags.Case) && sym.flags.is(Flags.Module)) + Expr(sym.name.stripSuffix("$")) + else + Expr(sym.name) + + val typeHint = sym.annotations.map(findTypeHint).find(_.isDefined).flatten match { + case Some(th) => '{ Some($th) } + case None => '{ None } + } + + '{ + TypeMetaData( + name = $name, + typeHintRaw = $typeHint, + fields = Vector($fields*) + ) + } } def readTraitMetaData[T: Type]: Expr[TraitMetaData] = { @@ -123,24 +169,6 @@ class AnnotationReader(using q: Quotes) { } } - private def typeMetaData(sym: Symbol): Expr[TypeMetaData] = { - val caseParams = sym.primaryConstructor.paramSymss.take(1).flatten - val fields = Varargs(caseParams.zipWithIndex.map(collectFieldInfo(sym.companionModule))) - val name = Expr(sym.name) - val typeHint = sym.annotations.map(findTypeHint).find(_.isDefined).flatten match { - case Some(th) => '{ Some($th) } - case None => '{ None } - } - - '{ - TypeMetaData( - name = $name, - typeHintRaw = $typeHint, - fields = Vector($fields*) - ) - } - } - private def subtypeAnnotation(sym: Symbol): Expr[(String, TypeMetaData)] = { val name = Expr(sym.name) val annots = typeMetaData(sym) diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala index 50c6e671..e19ec8d5 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala @@ -1,11 +1,11 @@ package io.sphere.mongo.generic import com.mongodb.BasicDBObject -import io.sphere.mongo.format.MongoFormat +import io.sphere.mongo.format.{MongoFormat, TraitMongoFormat} import org.bson.BSONObject import scala.deriving.Mirror -import scala.compiletime.{erasedValue, error, summonInline} +import scala.compiletime.{constValueTuple, erasedValue, error, summonInline} inline def deriveMongoFormat[A](using Mirror.Of[A]): MongoFormat[A] = MongoFormat.derived @@ -15,28 +15,45 @@ def mongoEnum(e: Enumeration): MongoFormat[e.Value] = new MongoFormat[e.Value] { def fromMongoValue(any: Any): e.Value = e.withName(any.asInstanceOf[String]) } -inline def mongoTypeSwitch[SuperType, SubTypeTuple <: Tuple](): MongoFormat[SuperType] = { +inline def mongoTypeSwitch[SuperType, SubTypeTuple <: Tuple]: MongoFormat[SuperType] = { val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] val typeHintMap = traitMetaData.subTypeTypeHints val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) - val formatters: Vector[MongoFormat[Any]] = summonFormatters[SubTypeTuple]() - val names = summonMetaData[SubTypeTuple]().map(_.name) - val formattersByTypeName = names.zip(formatters).toMap + val formatters = summonFormatters[SubTypeTuple]() + val subTypeNames = summonMetaData[SubTypeTuple]() - MongoFormat.instance( + val pairedFormatterWithSubtypeName = subTypeNames.map(_.name).zip(formatters) + val (caseClassFormatterList, traitFormatters) = pairedFormatterWithSubtypeName.partitionMap { + case kv @ (name, formatter) => + formatter match { + case traitFormatter: TraitMongoFormat[_] => Right(traitFormatter) + case _ => Left(kv) + } + } + val caseClassFormatters = caseClassFormatterList.toMap + + TraitMongoFormat.instance( toMongo = { a => - val originalTypeName = a.asInstanceOf[Product].productPrefix - val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) - val bson = - formattersByTypeName(originalTypeName).toMongoValue(a).asInstanceOf[BasicDBObject] - bson.put(traitMetaData.typeDiscriminator, typeName) - bson + traitFormatters.view.map(_.attemptWrite(a)).find(_.isSuccess).map(_.get) match { + case Some(bson) => bson + case None => + val originalTypeName = a.asInstanceOf[Product].productPrefix + val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) + val bson = + caseClassFormatters(originalTypeName).toMongoValue(a).asInstanceOf[BasicDBObject] + bson.put(traitMetaData.typeDiscriminator, typeName) + bson + } }, fromMongo = { case bson: BasicDBObject => - val typeName = bson.get(traitMetaData.typeDiscriminator).asInstanceOf[String] - val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - formattersByTypeName(originalTypeName).fromMongoValue(bson).asInstanceOf[SuperType] + traitFormatters.view.map(_.attemptRead(bson)).find(_.isSuccess).map(_.get) match { + case Some(a) => a.asInstanceOf[SuperType] + case None => + val typeName = bson.get(traitMetaData.typeDiscriminator).asInstanceOf[String] + val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) + caseClassFormatters(originalTypeName).fromMongoValue(bson).asInstanceOf[SuperType] + } case x => throw new Exception(s"BsonObject is expected for a Trait subtype, instead got $x") } diff --git a/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/SerializationTest.scala b/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/SerializationTest.scala index 4b309d5f..83eb04a2 100644 --- a/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/SerializationTest.scala +++ b/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/SerializationTest.scala @@ -2,7 +2,8 @@ package io.sphere.mongo import com.mongodb.BasicDBObject import io.sphere.mongo.format.{DefaultMongoFormats, MongoFormat} -import io.sphere.mongo.generic.AnnotationReader +import io.sphere.mongo.generic.{AnnotationReader, MongoTypeHint} +import io.sphere.mongo.MongoUtils.dbObj import DefaultMongoFormats.given import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -36,6 +37,7 @@ object SumTypes { enum Visitor derives MongoFormat { case User(email: String, password: String) case Anonymous + @MongoTypeHint("Admin") case Administrator } } @@ -148,29 +150,22 @@ class SerializationTest extends AnyWordSpec with Matchers { "serialize and deserialize enums" in { val mongo = MongoFormat[Visitor] - val anonObj = { - val dbo = new BasicDBObject - dbo.put("type", "Anonymous") - dbo - } val serializedAnon = mongo.toMongoValue(Visitor.Anonymous) val deserializedAnon = mongo.fromMongoValue(serializedAnon) - serializedAnon must be(anonObj) + serializedAnon must be(dbObj("type" -> "Anonymous")) deserializedAnon must be(Visitor.Anonymous) + val serializedAdmin = mongo.toMongoValue(Visitor.Administrator) + val deserializedAdmin = mongo.fromMongoValue(serializedAdmin) + serializedAdmin must be(dbObj("type" -> "Admin")) + deserializedAdmin must be(Visitor.Administrator) + val email = "ian@sosafe.com" val password = "123456" val user = Visitor.User(email, password) - val userObj = { - val dbo = new BasicDBObject - dbo.put("email", email) - dbo.put("password", password) - dbo.put("type", "User") - dbo - } val serializedUser = mongo.toMongoValue(user) - val deserializedUser = mongo.fromMongoValue(userObj) - serializedUser must be(userObj) + val deserializedUser = mongo.fromMongoValue(serializedUser) + serializedUser must be(dbObj("email" -> email, "password" -> password, "type" -> "User")) deserializedUser must be(user) } diff --git a/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala b/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala index 56bda822..7a479929 100644 --- a/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala +++ b/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/generic/MongoTypeSwitchSpec.scala @@ -16,7 +16,7 @@ class MongoTypeSwitchSpec extends AnyWordSpec with Matchers { "mongoTypeSwitch" must { "derive a subset of a sealed trait" in { - val format = mongoTypeSwitch[A, (B, C)]() + val format = mongoTypeSwitch[A, (B, C)] val b = B(123) val bson = format.toMongoValue(b) @@ -34,7 +34,7 @@ class MongoTypeSwitchSpec extends AnyWordSpec with Matchers { } "derive a subset of a sealed trait with a mongoKey" in { - val format = mongoTypeSwitch[A, (B, D)]() + val format = mongoTypeSwitch[A, (B, D)] val d = D(123) val bson = format.toMongoValue(d).asInstanceOf[BSONObject] From 9e6a6eb6f8ee0b96c3a5d0215a1af65d2328bab1 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Sat, 10 May 2025 11:34:42 +0200 Subject: [PATCH 118/142] add enum object support to json-annotationreader too. --- .../generic/AnnotationReader.scala | 71 ++++++++++++------- .../mongo/generic/AnnotationReader.scala | 6 +- 2 files changed, 48 insertions(+), 29 deletions(-) diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/AnnotationReader.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/AnnotationReader.scala index 02771a7c..259fe65d 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/AnnotationReader.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/AnnotationReader.scala @@ -5,7 +5,7 @@ import io.sphere.json.generic.JSONTypeHint import scala.quoted.{Expr, Quotes, Type, Varargs} -private type MA = JSONAnnotation +private type JA = JSONAnnotation case class Field( name: String, @@ -50,8 +50,50 @@ class AnnotationReader(using q: Quotes) { import q.reflect.* def readTypeMetaData[T: Type]: Expr[TypeMetaData] = { - val sym = TypeRepr.of[T].typeSymbol - typeMetaData(sym) + val tpe = TypeRepr.of[T] + val termSym = tpe.termSymbol + val typeSym = tpe.typeSymbol + if (termSym.flags.is(Flags.Enum) && typeSym.flags.is(Flags.Enum)) + typeMetaDataForEnumObjects(termSym) + else + typeMetaData(typeSym) + } + + private def typeMetaDataForEnumObjects(sym: Symbol): Expr[TypeMetaData] = { + val name = Expr(sym.name) + val typeHint = sym.annotations.map(findTypeHint).find(_.isDefined).flatten match { + case Some(th) => '{ Some($th) } + case None => '{ None } + } + '{ + TypeMetaData( + name = $name, + typeHintRaw = $typeHint, + fields = Vector.empty + ) + } + } + + private def typeMetaData(sym: Symbol): Expr[TypeMetaData] = { + val caseParams = sym.primaryConstructor.paramSymss.take(1).flatten + val fields = Varargs(caseParams.zipWithIndex.map(collectFieldInfo(sym.companionModule))) + val name = + if (sym.flags.is(Flags.Case) && sym.flags.is(Flags.Module)) + Expr(sym.name.stripSuffix("$")) + else + Expr(sym.name) + val typeHint = sym.annotations.map(findTypeHint).find(_.isDefined).flatten match { + case Some(th) => '{ Some($th) } + case None => '{ None } + } + + '{ + TypeMetaData( + name = $name, + typeHintRaw = $typeHint, + fields = Vector($fields*) + ) + } } def readTraitMetaData[T: Type]: Expr[TraitMetaData] = { @@ -71,8 +113,8 @@ class AnnotationReader(using q: Quotes) { } } - private def annotationTree(tree: Tree): Option[Expr[MA]] = - Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[MA]).map(_.asExprOf[MA]) + private def annotationTree(tree: Tree): Option[Expr[JA]] = + Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[JA]).map(_.asExprOf[JA]) private def findEmbedded(tree: Tree): Boolean = Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[JSONEmbedded]).isDefined @@ -121,25 +163,6 @@ class AnnotationReader(using q: Quotes) { } } - private def typeMetaData(sym: Symbol): Expr[TypeMetaData] = { - val caseParams = sym.primaryConstructor.paramSymss.take(1).flatten - val fields = Varargs(caseParams.zipWithIndex.map(collectFieldInfo(sym.companionModule))) - // Removing $ from the end of object names (productPrefix doesn't return names like that, so it's better not to have it) - val name = Expr(sym.name.stripSuffix("$")) - val typeHint = sym.annotations.map(findTypeHint).find(_.isDefined).flatten match { - case Some(th) => '{ Some($th) } - case None => '{ None } - } - - '{ - TypeMetaData( - name = $name, - typeHintRaw = $typeHint, - fields = Vector($fields*) - ) - } - } - private def subtypeAnnotation(sym: Symbol): Expr[(String, TypeMetaData)] = { val name = Expr(sym.name) val annots = typeMetaData(sym) diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/AnnotationReader.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/AnnotationReader.scala index e4aa1f4b..20a1a9f2 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/AnnotationReader.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/AnnotationReader.scala @@ -1,7 +1,6 @@ package io.sphere.mongo.generic import scala.quoted.{Expr, Quotes, Type, Varargs} -import io.sphere.mongo.format.MongoFormat private type MA = MongoAnnotation @@ -59,7 +58,6 @@ class AnnotationReader(using q: Quotes) { typeMetaDataForEnumObjects(termSym) else typeMetaData(typeSym) - } private def typeMetaDataForEnumObjects(sym: Symbol): Expr[TypeMetaData] = { @@ -81,9 +79,7 @@ class AnnotationReader(using q: Quotes) { val caseParams = sym.primaryConstructor.paramSymss.take(1).flatten val fields = Varargs(caseParams.zipWithIndex.map(collectFieldInfo(sym.companionModule))) val name = - if (sym.flags.is(Flags.Enum)) { - Expr(sym.name) - } else if (sym.flags.is(Flags.Case) && sym.flags.is(Flags.Module)) + if (sym.flags.is(Flags.Case) && sym.flags.is(Flags.Module)) Expr(sym.name.stripSuffix("$")) else Expr(sym.name) From 53042709fb905d875c892f97d1586ff921f021e0 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Sat, 10 May 2025 16:41:54 +0200 Subject: [PATCH 119/142] Refactor AnnotationReader so common parts can be used by both json and mongo --- .../generic/DeriveFromJSON.scala | 7 +- .../generic/DeriveSingleton.scala | 1 + .../io.sphere.json/generic/DeriveToJSON.scala | 5 +- .../generic/JSONTypeSwitch.scala | 5 +- .../generic/JsonAnnotationReader.scala | 46 +++++ .../io/sphere/mongo/format/MongoFormat.scala | 24 +-- .../mongo/generic/AnnotationReader.scala | 178 ------------------ .../mongo/generic/MongoAnnotationReader.scala | 48 +++++ .../io/sphere/mongo/generic/generic.scala | 9 +- .../io/sphere/mongo/DerivationSpec.scala | 4 +- .../io/sphere/mongo/SerializationTest.scala | 2 +- .../src/main/scala-3}/AnnotationReader.scala | 106 ++++------- util/src/main/scala-3/VectorUtils.scala | 19 -- 13 files changed, 161 insertions(+), 293 deletions(-) create mode 100644 json/json-core/src/main/scala-3/io.sphere.json/generic/JsonAnnotationReader.scala delete mode 100644 mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/AnnotationReader.scala create mode 100644 mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/MongoAnnotationReader.scala rename {json/json-core/src/main/scala-3/io.sphere.json/generic => util/src/main/scala-3}/AnnotationReader.scala (62%) delete mode 100644 util/src/main/scala-3/VectorUtils.scala diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala index 6107c22a..db65d1b8 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala @@ -3,6 +3,7 @@ package io.sphere.json.generic import cats.data.Validated import cats.syntax.traverse.* import io.sphere.json.* +import io.sphere.util.{Field, TypeMetaData} import org.json4s.JsonAST.* import scala.deriving.Mirror @@ -26,13 +27,13 @@ trait DeriveFromJSON { val fieldsAndJsons: Vector[(Field, FromJSON[Any])] = caseClassMetaData.fields.zip(fromJsons) val fieldNames: Vector[String] = fieldsAndJsons.flatMap { (field, fromJson) => - if (field.embedded) fromJson.fields.toVector :+ field.name - else Vector(field.name) + if (field.embedded) fromJson.fields.toVector :+ field.scalaName + else Vector(field.scalaName) } def readField(field: Field, fromJson: FromJSON[Any], jObject: JObject): JValidation[Any] = if (field.embedded) fromJson.read(jObject) - else io.sphere.json.field(field.fieldName, field.defaultArgument)(jObject)(fromJson) + else io.sphere.json.field(field.serializedName, field.defaultArgument)(jObject)(fromJson) FromJSON.instance( readFn = { diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala index 1f8f4562..e3892bf4 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala @@ -2,6 +2,7 @@ package io.sphere.json.generic import cats.data.Validated import io.sphere.json.{JSON, JSONParseError, JValidation} +import io.sphere.util.TraitMetaData import org.json4s.{JNull, JString, JValue} import scala.deriving.Mirror diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala index 70b8f572..161a12b8 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala @@ -1,6 +1,7 @@ package io.sphere.json.generic import io.sphere.json.ToJSON +import io.sphere.util.{Field, TypeMetaData} import org.json4s.JsonAST.* import scala.deriving.Mirror @@ -47,8 +48,8 @@ trait DeriveToJSON { jValue match { case o: JObject => if (field.embedded) JObject(jObject.obj ++ o.obj) - else JObject(jObject.obj :+ (field.fieldName -> o)) - case other => JObject(jObject.obj :+ (field.fieldName -> other)) + else JObject(jObject.obj :+ (field.serializedName -> o)) + case other => JObject(jObject.obj :+ (field.serializedName -> other)) } } diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/JSONTypeSwitch.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/JSONTypeSwitch.scala index ea115e77..467a848b 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/JSONTypeSwitch.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/JSONTypeSwitch.scala @@ -2,6 +2,7 @@ package io.sphere.json.generic import cats.data.Validated import io.sphere.json.{FromJSON, JSON, JSONParseError, ToJSON} +import io.sphere.util.TraitMetaData import org.json4s.{JObject, JString, jvalue2monadic, jvalue2readerSyntax} import org.json4s.DefaultJsonFormats.given @@ -23,10 +24,10 @@ object JSONTypeSwitch { val (traitInTraitInfo, caseClassFormatters) = formattersAndMetaData.partitionMap { (meta, formatter) => if (meta.isTrait) { - val formatterByName = meta.subtypes.map((fieldName, m) => m.name -> formatter) + val formatterByName = meta.subtypes.map((fieldName, m) => m.scalaName -> formatter) Left(formatterByName -> meta.subTypeFieldRenames) } else { - Right(meta.top.name -> formatter) + Right(meta.top.scalaName -> formatter) } } diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/JsonAnnotationReader.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/JsonAnnotationReader.scala new file mode 100644 index 00000000..38a9297d --- /dev/null +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/JsonAnnotationReader.scala @@ -0,0 +1,46 @@ +package io.sphere.json.generic + +import io.sphere.util.{AnnotationReader, TraitMetaData, TypeMetaData} + +import scala.quoted.{Expr, Quotes, Type} + +object AnnotationReader { + inline def readTypeMetaData[T]: TypeMetaData = ${ readTypeMetaDataImpl[T] } + + inline def readTraitMetaData[T]: TraitMetaData = ${ readTraitMetaDataImpl[T] } + + private def readTypeMetaDataImpl[T: Type](using Quotes): Expr[TypeMetaData] = + JsonAnnotationReader().readTypeMetaData[T] + + private def readTraitMetaDataImpl[T: Type](using Quotes): Expr[TraitMetaData] = + JsonAnnotationReader().readTraitMetaData[T] +} + +class JsonAnnotationReader(using q: Quotes) { + import q.reflect.* + private def findAnnotation[JA <: JSONAnnotation: Type](tree: Tree): Option[Expr[Any]] = + Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[JA]) + + private def embeddedPresent(tree: Tree): Boolean = + findAnnotation[JSONEmbedded](tree).isDefined + + private def findIgnored(tree: Tree): Boolean = + findAnnotation[JSONIgnore](tree).isDefined + + private def findKey(tree: Tree): Option[Expr[String]] = + findAnnotation[JSONKey](tree).map(_.asExprOf[JSONKey]).map(a => '{ $a.value }) + + private def findTypeHint(tree: Tree): Option[Expr[String]] = + findAnnotation[JSONTypeHint](tree).map(_.asExprOf[JSONTypeHint]).map(a => '{ $a.value }) + + private def findJSONTypeHintField(tree: Tree): Option[Expr[String]] = + findAnnotation[JSONTypeHintField](tree) + .map(_.asExprOf[JSONTypeHintField]) + .map(a => '{ $a.value }) + + private val annotationReader = + new AnnotationReader(embeddedPresent, findIgnored, findKey, findTypeHint, findJSONTypeHintField) + + export annotationReader.readTypeMetaData + export annotationReader.readTraitMetaData +} diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala index fc2b9429..2d01c13c 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/format/MongoFormat.scala @@ -1,15 +1,15 @@ package io.sphere.mongo.format import com.mongodb.BasicDBObject -import io.sphere.mongo.generic.{AnnotationReader, Field, mongoTypeSwitch} -import io.sphere.util.VectorUtils.* +import io.sphere.mongo.generic.{MongoAnnotationReader, mongoTypeSwitch} +import io.sphere.util.Field import org.bson.BSONObject import org.bson.types.ObjectId import java.util.UUID import java.util.regex.Pattern import scala.deriving.Mirror -import scala.util.{Success, Try} +import scala.util.Try type SimpleMongoType = UUID | String | ObjectId | Short | Int | Long | Float | Double | Boolean | Pattern @@ -69,13 +69,13 @@ object MongoFormat { } inline private def deriveCaseClass[A](mirrorOfProduct: Mirror.ProductOf[A]): MongoFormat[A] = { - val caseClassMetaData = AnnotationReader.readTypeMetaData[A] + val caseClassMetaData = MongoAnnotationReader.readTypeMetaData[A] val formatters = summonFormatters[mirrorOfProduct.MirroredElemTypes] val fieldsAndFormatters = caseClassMetaData.fields.zip(formatters) val fields: Set[String] = fieldsAndFormatters.toSet.flatMap((field, formatter) => - if (field.embedded) formatter.fields + field.rawName - else Set(field.rawName)) + if (field.embedded) formatter.fields + field.scalaName + else Set(field.scalaName)) instance( toMongo = { a => @@ -112,10 +112,10 @@ object MongoFormat { private def addField(bson: BasicDBObject, field: Field, mongoType: Any): Unit = if (!field.ignored) mongoType match { - case s: SimpleMongoType => bson.put(field.name, s) + case s: SimpleMongoType => bson.put(field.serializedName, s) case innerBson: BasicDBObject => if (field.embedded) innerBson.entrySet().forEach(p => bson.put(p.getKey, p.getValue)) - else bson.put(field.name, innerBson) + else bson.put(field.serializedName, innerBson) case MongoNothing => } @@ -123,15 +123,17 @@ object MongoFormat { def defaultValue = field.defaultArgument.orElse(format.default) if (field.ignored) defaultValue.getOrElse { - throw new Exception(s"Ignored Mongo field '${field.name}' must have a default value.") + throw new Exception( + s"Ignored Mongo field '${field.serializedName}' must have a default value.") } else if (field.embedded) format.fromMongoValue(bson) else { - val value = bson.get(field.name) + val value = bson.get(field.serializedName) if (value ne null) format.fromMongoValue(value.asInstanceOf[Any]) else defaultValue.getOrElse { - throw new Exception(s"Missing required field '${field.name}' on deserialization.") + throw new Exception( + s"Missing required field '${field.serializedName}' on deserialization.") } } } diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/AnnotationReader.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/AnnotationReader.scala deleted file mode 100644 index 20a1a9f2..00000000 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/AnnotationReader.scala +++ /dev/null @@ -1,178 +0,0 @@ -package io.sphere.mongo.generic - -import scala.quoted.{Expr, Quotes, Type, Varargs} - -private type MA = MongoAnnotation - -case class Field( - rawName: String, - embedded: Boolean, - ignored: Boolean, - mongoKey: Option[MongoKey], - defaultArgument: Option[Any]) { - val name: String = mongoKey.map(_.value).getOrElse(rawName) -} -case class TypeMetaData( - name: String, - typeHintRaw: Option[MongoTypeHint], - fields: Vector[Field] -) { - val typeHint: Option[String] = - typeHintRaw.map(_.value).filterNot(_.toList.forall(_ == ' ')) -} - -case class TraitMetaData( - top: TypeMetaData, - typeHintFieldRaw: Option[MongoTypeHintField], - subtypes: Map[String, TypeMetaData] -) { - val typeDiscriminator: String = typeHintFieldRaw.map(_.value).getOrElse("type") - - val subTypeTypeHints: Map[String, String] = subtypes.collect { - case (name, classMeta) if classMeta.typeHint.isDefined => - name -> classMeta.typeHint.get - } -} - -object AnnotationReader { - - inline def readTraitMetaData[T]: TraitMetaData = ${ readTraitMetaDataImpl[T] } - - inline def readTypeMetaData[T]: TypeMetaData = ${ readTypeMetaDataImpl[T] } - - private def readTypeMetaDataImpl[T: Type](using Quotes): Expr[TypeMetaData] = - AnnotationReader().readTypeMetaData[T] - - private def readTraitMetaDataImpl[T: Type](using Quotes): Expr[TraitMetaData] = - AnnotationReader().readTraitMetaData[T] -} - -class AnnotationReader(using q: Quotes) { - import q.reflect.* - - def readTypeMetaData[T: Type]: Expr[TypeMetaData] = { - val tpe = TypeRepr.of[T] - val termSym = tpe.termSymbol - val typeSym = tpe.typeSymbol - if (termSym.flags.is(Flags.Enum) && typeSym.flags.is(Flags.Enum)) - typeMetaDataForEnumObjects(termSym) - else - typeMetaData(typeSym) - } - - private def typeMetaDataForEnumObjects(sym: Symbol): Expr[TypeMetaData] = { - val name = Expr(sym.name) - val typeHint = sym.annotations.map(findTypeHint).find(_.isDefined).flatten match { - case Some(th) => '{ Some($th) } - case None => '{ None } - } - '{ - TypeMetaData( - name = $name, - typeHintRaw = $typeHint, - fields = Vector.empty - ) - } - } - - private def typeMetaData(sym: Symbol): Expr[TypeMetaData] = { - val caseParams = sym.primaryConstructor.paramSymss.take(1).flatten - val fields = Varargs(caseParams.zipWithIndex.map(collectFieldInfo(sym.companionModule))) - val name = - if (sym.flags.is(Flags.Case) && sym.flags.is(Flags.Module)) - Expr(sym.name.stripSuffix("$")) - else - Expr(sym.name) - - val typeHint = sym.annotations.map(findTypeHint).find(_.isDefined).flatten match { - case Some(th) => '{ Some($th) } - case None => '{ None } - } - - '{ - TypeMetaData( - name = $name, - typeHintRaw = $typeHint, - fields = Vector($fields*) - ) - } - } - - def readTraitMetaData[T: Type]: Expr[TraitMetaData] = { - val sym = TypeRepr.of[T].typeSymbol - val typeHintField = - sym.annotations.map(findMongoTypeHintField).find(_.isDefined).flatten match { - case Some(thf) => '{ Some($thf) } - case None => '{ None } - } - - '{ - TraitMetaData( - top = ${ typeMetaData(sym) }, - typeHintFieldRaw = $typeHintField, - subtypes = ${ subtypeAnnotations(sym) } - ) - } - } - - private def annotationTree(tree: Tree): Option[Expr[MA]] = - Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[MA]).map(_.asExprOf[MA]) - - private def findEmbedded(tree: Tree): Boolean = - Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[MongoEmbedded]).isDefined - - private def findIgnored(tree: Tree): Boolean = - Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[MongoIgnore]).isDefined - - private def findKey(tree: Tree): Option[Expr[MongoKey]] = - Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[MongoKey]).map(_.asExprOf[MongoKey]) - - private def findTypeHint(tree: Tree): Option[Expr[MongoTypeHint]] = - Option - .when(tree.isExpr)(tree.asExpr) - .filter(_.isExprOf[MongoTypeHint]) - .map(_.asExprOf[MongoTypeHint]) - - private def findMongoTypeHintField(tree: Tree): Option[Expr[MongoTypeHintField]] = - Option - .when(tree.isExpr)(tree.asExpr) - .filter(_.isExprOf[MongoTypeHintField]) - .map(_.asExprOf[MongoTypeHintField]) - - private def collectFieldInfo(companion: Symbol)(s: Symbol, paramIdx: Int): Expr[Field] = { - val embedded = Expr(s.annotations.exists(findEmbedded)) - val ignored = Expr(s.annotations.exists(findIgnored)) - val name = Expr(s.name) - val mongoKey = s.annotations.map(findKey).find(_.isDefined).flatten match { - case Some(k) => '{ Some($k) } - case None => '{ None } - } - val defArgOpt = companion - .methodMember(s"$$lessinit$$greater$$default$$${paramIdx + 1}") - .headOption - .map(dm => Ref(dm).asExprOf[Any]) match { - case Some(k) => '{ Some($k) } - case None => '{ None } - } - - '{ - Field( - rawName = $name, - embedded = $embedded, - ignored = $ignored, - mongoKey = $mongoKey, - defaultArgument = $defArgOpt) - } - } - - private def subtypeAnnotation(sym: Symbol): Expr[(String, TypeMetaData)] = { - val name = Expr(sym.name) - val annots = typeMetaData(sym) - '{ ($name, $annots) } - } - - private def subtypeAnnotations(sym: Symbol): Expr[Map[String, TypeMetaData]] = { - val subtypes = Varargs(sym.children.map(subtypeAnnotation)) - '{ Map($subtypes*) } - } -} diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/MongoAnnotationReader.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/MongoAnnotationReader.scala new file mode 100644 index 00000000..e0f5e32c --- /dev/null +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/MongoAnnotationReader.scala @@ -0,0 +1,48 @@ +package io.sphere.mongo.generic + +import io.sphere.util.{AnnotationReader, TraitMetaData, TypeMetaData} + +import scala.quoted.{Expr, Quotes, Type, Varargs} + +object MongoAnnotationReader { + + inline def readTraitMetaData[T]: TraitMetaData = ${ readTraitMetaDataImpl[T] } + + inline def readTypeMetaData[T]: TypeMetaData = ${ readTypeMetaDataImpl[T] } + + private def readTypeMetaDataImpl[T: Type](using Quotes): Expr[TypeMetaData] = + MongoAnnotationReader().readTypeMetaData[T] + + private def readTraitMetaDataImpl[T: Type](using Quotes): Expr[TraitMetaData] = + MongoAnnotationReader().readTraitMetaData[T] +} + +class MongoAnnotationReader(using q: Quotes) { + import q.reflect.* + + private def findAnnotation[MA <: MongoAnnotation: Type](tree: Tree): Option[Expr[Any]] = + Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[MA]) + + private def findEmbedded(tree: Tree): Boolean = + findAnnotation[MongoEmbedded](tree).isDefined + + private def findIgnored(tree: Tree): Boolean = + findAnnotation[MongoIgnore](tree).isDefined + + private def findKey(tree: Tree): Option[Expr[String]] = + findAnnotation[MongoKey](tree).map(_.asExprOf[MongoKey]).map(a => '{ $a.value }) + + private def findTypeHint(tree: Tree): Option[Expr[String]] = + findAnnotation[MongoTypeHint](tree).map(_.asExprOf[MongoTypeHint]).map(a => '{ $a.value }) + + private def findMongoTypeHintField(tree: Tree): Option[Expr[String]] = + findAnnotation[MongoTypeHintField](tree) + .map(_.asExprOf[MongoTypeHintField]) + .map(a => '{ $a.value }) + + private val annotationReader = + new AnnotationReader(findEmbedded, findIgnored, findKey, findTypeHint, findMongoTypeHintField) + export annotationReader.readTraitMetaData + export annotationReader.readTypeMetaData + +} diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala index e19ec8d5..866dfe1d 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala @@ -2,6 +2,7 @@ package io.sphere.mongo.generic import com.mongodb.BasicDBObject import io.sphere.mongo.format.{MongoFormat, TraitMongoFormat} +import io.sphere.util.TypeMetaData import org.bson.BSONObject import scala.deriving.Mirror @@ -16,13 +17,13 @@ def mongoEnum(e: Enumeration): MongoFormat[e.Value] = new MongoFormat[e.Value] { } inline def mongoTypeSwitch[SuperType, SubTypeTuple <: Tuple]: MongoFormat[SuperType] = { - val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] - val typeHintMap = traitMetaData.subTypeTypeHints + val traitMetaData = MongoAnnotationReader.readTraitMetaData[SuperType] + val typeHintMap = traitMetaData.subTypeFieldRenames val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) val formatters = summonFormatters[SubTypeTuple]() val subTypeNames = summonMetaData[SubTypeTuple]() - val pairedFormatterWithSubtypeName = subTypeNames.map(_.name).zip(formatters) + val pairedFormatterWithSubtypeName = subTypeNames.map(_.scalaName).zip(formatters) val (caseClassFormatterList, traitFormatters) = pairedFormatterWithSubtypeName.partitionMap { case kv @ (name, formatter) => formatter match { @@ -68,7 +69,7 @@ inline private def summonMetaData[T <: Tuple]( inline erasedValue[T] match { case _: EmptyTuple => acc case _: (t *: ts) => - summonMetaData[ts](acc :+ AnnotationReader.readTypeMetaData[t]) + summonMetaData[ts](acc :+ MongoAnnotationReader.readTypeMetaData[t]) } inline private def summonFormatters[T <: Tuple]( diff --git a/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/DerivationSpec.scala b/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/DerivationSpec.scala index 692fc4fa..479b7cba 100644 --- a/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/DerivationSpec.scala +++ b/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/DerivationSpec.scala @@ -1,6 +1,6 @@ package io.sphere.mongo -import io.sphere.mongo.generic.{AnnotationReader, MongoEmbedded, MongoKey, MongoTypeHintField} +import io.sphere.mongo.generic.{MongoAnnotationReader, MongoEmbedded, MongoKey, MongoTypeHintField} import io.sphere.mongo.format.DefaultMongoFormats.given import org.scalatest.wordspec.AnyWordSpec import org.scalatest.matchers.must.Matchers @@ -50,7 +50,7 @@ class DerivationSpec extends AnyWordSpec with Matchers { case object Object2 extends Root case class Class(i: Int, @MongoEmbedded inner: InnerClass) extends Root - val res = AnnotationReader.readTypeMetaData[Root] + val res = MongoAnnotationReader.readTypeMetaData[Root] } } diff --git a/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/SerializationTest.scala b/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/SerializationTest.scala index 83eb04a2..bff38c11 100644 --- a/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/SerializationTest.scala +++ b/mongo/mongo-derivation/src/test/scala-3/io/sphere/mongo/SerializationTest.scala @@ -2,7 +2,7 @@ package io.sphere.mongo import com.mongodb.BasicDBObject import io.sphere.mongo.format.{DefaultMongoFormats, MongoFormat} -import io.sphere.mongo.generic.{AnnotationReader, MongoTypeHint} +import io.sphere.mongo.generic.{MongoAnnotationReader, MongoTypeHint} import io.sphere.mongo.MongoUtils.dbObj import DefaultMongoFormats.given import org.scalatest.matchers.must.Matchers diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/AnnotationReader.scala b/util/src/main/scala-3/AnnotationReader.scala similarity index 62% rename from json/json-core/src/main/scala-3/io.sphere.json/generic/AnnotationReader.scala rename to util/src/main/scala-3/AnnotationReader.scala index 259fe65d..a8362c80 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/AnnotationReader.scala +++ b/util/src/main/scala-3/AnnotationReader.scala @@ -1,28 +1,23 @@ -package io.sphere.json.generic - -import io.sphere.json.generic.JSONAnnotation -import io.sphere.json.generic.JSONTypeHint +package io.sphere.util import scala.quoted.{Expr, Quotes, Type, Varargs} -private type JA = JSONAnnotation - case class Field( - name: String, + scalaName: String, embedded: Boolean, ignored: Boolean, - jsonKey: Option[JSONKey], + key: Option[String], defaultArgument: Option[Any]) { - val fieldName: String = jsonKey.map(_.value).getOrElse(name) + val serializedName: String = key.getOrElse(scalaName) } case class TypeMetaData( - name: String, - typeHintRaw: Option[JSONTypeHint], + scalaName: String, + typeHintRaw: Option[String], fields: Vector[Field] ) { val typeHint: Option[String] = - typeHintRaw.map(_.value).filterNot(_.toList.forall(_ == ' ')) + typeHintRaw.filterNot(_.toList.forall(_ == ' ')) } /** This class also works for case classes not only traits, in case of case classes only the `top` @@ -30,14 +25,14 @@ case class TypeMetaData( */ case class TraitMetaData( top: TypeMetaData, - typeHintFieldRaw: Option[JSONTypeHintField], + typeHintFieldRaw: Option[String], subtypes: Map[String, TypeMetaData] ) { def isTrait: Boolean = subtypes.nonEmpty private val defaultTypeDiscriminatorName = "type" val typeDiscriminator: String = - typeHintFieldRaw.map(_.value).getOrElse(defaultTypeDiscriminatorName) + typeHintFieldRaw.getOrElse(defaultTypeDiscriminatorName) val subTypeFieldRenames: Map[String, String] = subtypes.collect { case (name, classMeta) if classMeta.typeHint.isDefined => @@ -45,8 +40,13 @@ case class TraitMetaData( } } -class AnnotationReader(using q: Quotes) { - +class AnnotationReader(using q: Quotes)( + findEmbedded: q.reflect.Tree => Boolean, + findIgnored: q.reflect.Tree => Boolean, + findKey: q.reflect.Tree => Option[Expr[String]], + findTypeHint: q.reflect.Tree => Option[Expr[String]], + findTypeHintField: q.reflect.Tree => Option[Expr[String]] +) { import q.reflect.* def readTypeMetaData[T: Type]: Expr[TypeMetaData] = { @@ -67,7 +67,7 @@ class AnnotationReader(using q: Quotes) { } '{ TypeMetaData( - name = $name, + scalaName = $name, typeHintRaw = $typeHint, fields = Vector.empty ) @@ -89,54 +89,13 @@ class AnnotationReader(using q: Quotes) { '{ TypeMetaData( - name = $name, + scalaName = $name, typeHintRaw = $typeHint, fields = Vector($fields*) ) } } - def readTraitMetaData[T: Type]: Expr[TraitMetaData] = { - val sym = TypeRepr.of[T].typeSymbol - val typeHintField = - sym.annotations.map(findJSONTypeHintField).find(_.isDefined).flatten match { - case Some(thf) => '{ Some($thf) } - case None => '{ None } - } - - '{ - TraitMetaData( - top = ${ typeMetaData(sym) }, - typeHintFieldRaw = $typeHintField, - subtypes = ${ subtypeAnnotations(sym) } - ) - } - } - - private def annotationTree(tree: Tree): Option[Expr[JA]] = - Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[JA]).map(_.asExprOf[JA]) - - private def findEmbedded(tree: Tree): Boolean = - Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[JSONEmbedded]).isDefined - - private def findIgnored(tree: Tree): Boolean = - Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[JSONIgnore]).isDefined - - private def findKey(tree: Tree): Option[Expr[JSONKey]] = - Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[JSONKey]).map(_.asExprOf[JSONKey]) - - private def findTypeHint(tree: Tree): Option[Expr[JSONTypeHint]] = - Option - .when(tree.isExpr)(tree.asExpr) - .filter(_.isExprOf[JSONTypeHint]) - .map(_.asExprOf[JSONTypeHint]) - - private def findJSONTypeHintField(tree: Tree): Option[Expr[JSONTypeHintField]] = - Option - .when(tree.isExpr)(tree.asExpr) - .filter(_.isExprOf[JSONTypeHintField]) - .map(_.asExprOf[JSONTypeHintField]) - private def collectFieldInfo(companion: Symbol)(s: Symbol, paramIdx: Int): Expr[Field] = { val embedded = Expr(s.annotations.exists(findEmbedded)) val ignored = Expr(s.annotations.exists(findIgnored)) @@ -155,10 +114,10 @@ class AnnotationReader(using q: Quotes) { '{ Field( - name = $name, + scalaName = $name, embedded = $embedded, ignored = $ignored, - jsonKey = $key, + key = $key, defaultArgument = $defArgOpt) } } @@ -174,16 +133,21 @@ class AnnotationReader(using q: Quotes) { '{ Map($subtypes*) } } -} - -object AnnotationReader { - inline def readTypeMetaData[T]: TypeMetaData = ${ readTypeMetaDataImpl[T] } - - inline def readTraitMetaData[T]: TraitMetaData = ${ readTraitMetaDataImpl[T] } + def readTraitMetaData[T: Type]: Expr[TraitMetaData] = { + val sym = TypeRepr.of[T].typeSymbol + val typeHintField = + sym.annotations.map(findTypeHintField).find(_.isDefined).flatten match { + case Some(thf) => '{ Some($thf) } + case None => '{ None } + } - private def readTypeMetaDataImpl[T: Type](using Quotes): Expr[TypeMetaData] = - AnnotationReader().readTypeMetaData[T] + '{ + TraitMetaData( + top = ${ typeMetaData(sym) }, + typeHintFieldRaw = $typeHintField, + subtypes = ${ subtypeAnnotations(sym) } + ) + } + } - private def readTraitMetaDataImpl[T: Type](using Quotes): Expr[TraitMetaData] = - AnnotationReader().readTraitMetaData[T] } diff --git a/util/src/main/scala-3/VectorUtils.scala b/util/src/main/scala-3/VectorUtils.scala deleted file mode 100644 index 5b9fe943..00000000 --- a/util/src/main/scala-3/VectorUtils.scala +++ /dev/null @@ -1,19 +0,0 @@ -package io.sphere.util - -object VectorUtils { - - extension [A](vector: Vector[A]) { - - // toMap by default will remove all but one of the key value pairs in case of duplicate keys - def toMapWithNoDuplicateKeys[K, V](using A <:< (K, V)): Map[K, V] = { - val duplicateKeys = - vector.groupBy(_._1).collect { case (key, values) if values.size >= 2 => key } - if (duplicateKeys.nonEmpty) - throw new Exception( - s"Cannot construct Map because the following keys are duplicates: ${duplicateKeys.mkString(", ")}") - else - vector.toMap - } - } - -} From a13cd6facd9c7e5b0fa87738bf6f529e6bfb3f09 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Sat, 10 May 2025 17:04:16 +0200 Subject: [PATCH 120/142] renames --- .../io.sphere.json/generic/JsonAnnotationReader.scala | 8 ++++---- .../io/sphere/mongo/generic/MongoAnnotationReader.scala | 8 ++++---- util/src/main/scala-3/AnnotationReader.scala | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/JsonAnnotationReader.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/JsonAnnotationReader.scala index 38a9297d..c955cbb6 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/JsonAnnotationReader.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/JsonAnnotationReader.scala @@ -21,10 +21,10 @@ class JsonAnnotationReader(using q: Quotes) { private def findAnnotation[JA <: JSONAnnotation: Type](tree: Tree): Option[Expr[Any]] = Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[JA]) - private def embeddedPresent(tree: Tree): Boolean = + private def embeddedExists(tree: Tree): Boolean = findAnnotation[JSONEmbedded](tree).isDefined - private def findIgnored(tree: Tree): Boolean = + private def ignoredExists(tree: Tree): Boolean = findAnnotation[JSONIgnore](tree).isDefined private def findKey(tree: Tree): Option[Expr[String]] = @@ -33,13 +33,13 @@ class JsonAnnotationReader(using q: Quotes) { private def findTypeHint(tree: Tree): Option[Expr[String]] = findAnnotation[JSONTypeHint](tree).map(_.asExprOf[JSONTypeHint]).map(a => '{ $a.value }) - private def findJSONTypeHintField(tree: Tree): Option[Expr[String]] = + private def findTypeHintField(tree: Tree): Option[Expr[String]] = findAnnotation[JSONTypeHintField](tree) .map(_.asExprOf[JSONTypeHintField]) .map(a => '{ $a.value }) private val annotationReader = - new AnnotationReader(embeddedPresent, findIgnored, findKey, findTypeHint, findJSONTypeHintField) + new AnnotationReader(embeddedExists, ignoredExists, findKey, findTypeHint, findTypeHintField) export annotationReader.readTypeMetaData export annotationReader.readTraitMetaData diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/MongoAnnotationReader.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/MongoAnnotationReader.scala index e0f5e32c..d9171842 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/MongoAnnotationReader.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/MongoAnnotationReader.scala @@ -23,10 +23,10 @@ class MongoAnnotationReader(using q: Quotes) { private def findAnnotation[MA <: MongoAnnotation: Type](tree: Tree): Option[Expr[Any]] = Option.when(tree.isExpr)(tree.asExpr).filter(_.isExprOf[MA]) - private def findEmbedded(tree: Tree): Boolean = + private def embeddedExists(tree: Tree): Boolean = findAnnotation[MongoEmbedded](tree).isDefined - private def findIgnored(tree: Tree): Boolean = + private def ignoredExists(tree: Tree): Boolean = findAnnotation[MongoIgnore](tree).isDefined private def findKey(tree: Tree): Option[Expr[String]] = @@ -35,13 +35,13 @@ class MongoAnnotationReader(using q: Quotes) { private def findTypeHint(tree: Tree): Option[Expr[String]] = findAnnotation[MongoTypeHint](tree).map(_.asExprOf[MongoTypeHint]).map(a => '{ $a.value }) - private def findMongoTypeHintField(tree: Tree): Option[Expr[String]] = + private def findTypeHintField(tree: Tree): Option[Expr[String]] = findAnnotation[MongoTypeHintField](tree) .map(_.asExprOf[MongoTypeHintField]) .map(a => '{ $a.value }) private val annotationReader = - new AnnotationReader(findEmbedded, findIgnored, findKey, findTypeHint, findMongoTypeHintField) + new AnnotationReader(embeddedExists, ignoredExists, findKey, findTypeHint, findTypeHintField) export annotationReader.readTraitMetaData export annotationReader.readTypeMetaData diff --git a/util/src/main/scala-3/AnnotationReader.scala b/util/src/main/scala-3/AnnotationReader.scala index a8362c80..b76c201a 100644 --- a/util/src/main/scala-3/AnnotationReader.scala +++ b/util/src/main/scala-3/AnnotationReader.scala @@ -41,8 +41,8 @@ case class TraitMetaData( } class AnnotationReader(using q: Quotes)( - findEmbedded: q.reflect.Tree => Boolean, - findIgnored: q.reflect.Tree => Boolean, + embeddedExists: q.reflect.Tree => Boolean, + ignoredExists: q.reflect.Tree => Boolean, findKey: q.reflect.Tree => Option[Expr[String]], findTypeHint: q.reflect.Tree => Option[Expr[String]], findTypeHintField: q.reflect.Tree => Option[Expr[String]] @@ -97,8 +97,8 @@ class AnnotationReader(using q: Quotes)( } private def collectFieldInfo(companion: Symbol)(s: Symbol, paramIdx: Int): Expr[Field] = { - val embedded = Expr(s.annotations.exists(findEmbedded)) - val ignored = Expr(s.annotations.exists(findIgnored)) + val embedded = Expr(s.annotations.exists(embeddedExists)) + val ignored = Expr(s.annotations.exists(ignoredExists)) val name = Expr(s.name) val key = s.annotations.map(findKey).find(_.isDefined).flatten match { case Some(k) => '{ Some($k) } From 0b585cd90712ba7ba7b2ee73d649e0713bc7eb92 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Sat, 10 May 2025 19:37:07 +0200 Subject: [PATCH 121/142] improve naming --- .../generic/DeriveSingleton.scala | 10 +++--- .../generic/JSONTypeSwitch.scala | 36 +++++++++++-------- .../io/sphere/mongo/generic/generic.scala | 14 ++++---- util/src/main/scala-3/AnnotationReader.scala | 4 +-- 4 files changed, 35 insertions(+), 29 deletions(-) diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala index e3892bf4..a50f4eb3 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala @@ -51,8 +51,8 @@ object DeriveSingleton { DeriveSingleton.instance( readFn = { case JString(typeName) => - val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - jsonsByNames.get(originalTypeName) match { + val scalaTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) + jsonsByNames.get(scalaTypeName) match { case Some(json) => val dummyValue = JNull json.read(dummyValue).map(_.asInstanceOf[A]) @@ -64,9 +64,9 @@ object DeriveSingleton { Validated.invalidNel(JSONParseError(s"JSON string expected. Got >>> $x")) }, writeFn = { value => - val originalTypeName = value.asInstanceOf[Product].productPrefix - val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) - JString(typeName) + val scalaTypeName = value.asInstanceOf[Product].productPrefix + val serializedTypeName = typeHintMap.getOrElse(scalaTypeName, scalaTypeName) + JString(serializedTypeName) } ) } diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/JSONTypeSwitch.scala b/json/json-core/src/main/scala-3/io.sphere.json/generic/JSONTypeSwitch.scala index 467a848b..58297b95 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/JSONTypeSwitch.scala +++ b/json/json-core/src/main/scala-3/io.sphere.json/generic/JSONTypeSwitch.scala @@ -48,20 +48,12 @@ object JSONTypeSwitch { inline def toJsonTypeSwitch[SuperType](info: TraitInformation): ToJSON[SuperType] = ToJSON.instance { a => - val originalTypeName = a.asInstanceOf[Product].productPrefix - val typeName = info.mergedTypeHintMap.getOrElse(originalTypeName, originalTypeName) - val traitFormatterOpt = info.traitInTraitFormatterMap.get(originalTypeName) + val scalaTypeName = a.asInstanceOf[Product].productPrefix + val serializedTypeName = info.mergedTypeHintMap.getOrElse(scalaTypeName, scalaTypeName) + val traitFormatterOpt = info.traitInTraitFormatterMap.get(scalaTypeName) traitFormatterOpt .map(_.write(a)) - .getOrElse { - val jsonObj = info.caseClassFormatterMap(originalTypeName).write(a) match { - case JObject(obj) => obj - case json => - throw new Exception(s"This code only handles objects as of now, but got: $json") - } - val typeDiscriminator = info.traitMetaData.typeDiscriminator -> JString(typeName) - JObject(typeDiscriminator :: jsonObj) - } + .getOrElse(writeCaseClass(info, scalaTypeName, a, serializedTypeName)) } inline def fromJsonTypeSwitch[SuperType](info: TraitInformation): FromJSON[SuperType] = { @@ -70,9 +62,9 @@ object JSONTypeSwitch { FromJSON.instance { case jObject: JObject => - val typeName = (jObject \ info.traitMetaData.typeDiscriminator).as[String] - val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - allFormattersByTypeName(originalTypeName).read(jObject).map(_.asInstanceOf[SuperType]) + val serializedTypeName = (jObject \ info.traitMetaData.typeDiscriminator).as[String] + val scalaTypeName = reverseTypeHintMap.getOrElse(serializedTypeName, serializedTypeName) + allFormattersByTypeName(scalaTypeName).read(jObject).map(_.asInstanceOf[SuperType]) case x => Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$x'")) } @@ -89,6 +81,20 @@ object JSONTypeSwitch { ) } + private def writeCaseClass[A]( + info: TraitInformation, + scalaTypeName: String, + a: A, + serializedTypeName: String): JObject = { + val jsonObj = info.caseClassFormatterMap(scalaTypeName).write(a) match { + case JObject(obj) => obj + case json => + throw new Exception(s"This code only handles objects as of now, but got: $json") + } + val typeDiscriminator = info.traitMetaData.typeDiscriminator -> JString(serializedTypeName) + JObject(typeDiscriminator :: jsonObj) + } + inline private def summonFormatters[T <: Tuple]( acc: Vector[(TraitMetaData, JSON[Any])] = Vector.empty): Vector[(TraitMetaData, JSON[Any])] = inline erasedValue[T] match { diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala index 866dfe1d..03628573 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala @@ -38,11 +38,11 @@ inline def mongoTypeSwitch[SuperType, SubTypeTuple <: Tuple]: MongoFormat[SuperT traitFormatters.view.map(_.attemptWrite(a)).find(_.isSuccess).map(_.get) match { case Some(bson) => bson case None => - val originalTypeName = a.asInstanceOf[Product].productPrefix - val typeName = typeHintMap.getOrElse(originalTypeName, originalTypeName) + val scalaTypeName = a.asInstanceOf[Product].productPrefix + val serializedTypeName = typeHintMap.getOrElse(scalaTypeName, scalaTypeName) val bson = - caseClassFormatters(originalTypeName).toMongoValue(a).asInstanceOf[BasicDBObject] - bson.put(traitMetaData.typeDiscriminator, typeName) + caseClassFormatters(scalaTypeName).toMongoValue(a).asInstanceOf[BasicDBObject] + bson.put(traitMetaData.typeDiscriminator, serializedTypeName) bson } }, @@ -51,9 +51,9 @@ inline def mongoTypeSwitch[SuperType, SubTypeTuple <: Tuple]: MongoFormat[SuperT traitFormatters.view.map(_.attemptRead(bson)).find(_.isSuccess).map(_.get) match { case Some(a) => a.asInstanceOf[SuperType] case None => - val typeName = bson.get(traitMetaData.typeDiscriminator).asInstanceOf[String] - val originalTypeName = reverseTypeHintMap.getOrElse(typeName, typeName) - caseClassFormatters(originalTypeName).fromMongoValue(bson).asInstanceOf[SuperType] + val serializedTypeName = bson.get(traitMetaData.typeDiscriminator).asInstanceOf[String] + val scalaTypeName = reverseTypeHintMap.getOrElse(serializedTypeName, serializedTypeName) + caseClassFormatters(scalaTypeName).fromMongoValue(bson).asInstanceOf[SuperType] } case x => throw new Exception(s"BsonObject is expected for a Trait subtype, instead got $x") diff --git a/util/src/main/scala-3/AnnotationReader.scala b/util/src/main/scala-3/AnnotationReader.scala index b76c201a..35db2952 100644 --- a/util/src/main/scala-3/AnnotationReader.scala +++ b/util/src/main/scala-3/AnnotationReader.scala @@ -35,8 +35,8 @@ case class TraitMetaData( typeHintFieldRaw.getOrElse(defaultTypeDiscriminatorName) val subTypeFieldRenames: Map[String, String] = subtypes.collect { - case (name, classMeta) if classMeta.typeHint.isDefined => - name -> classMeta.typeHint.get + case (scalaName, classMeta) if classMeta.typeHint.isDefined => + scalaName -> classMeta.typeHint.get } } From 941fe88791b57b019bf5f01860a9a0e4a73a1fd0 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 23 May 2025 14:16:36 +0200 Subject: [PATCH 122/142] Fix package structure --- .../scala-3/{io.sphere.json => io/sphere/json}/FromJSON.scala | 0 .../main/scala-3/{io.sphere.json => io/sphere/json}/JSON.scala | 0 .../scala-3/{io.sphere.json => io/sphere/json}/ToJSON.scala | 0 .../sphere/json}/generic/Annotations.scala | 0 .../sphere/json}/generic/DeriveFromJSON.scala | 0 .../sphere/json}/generic/DeriveSingleton.scala | 0 .../sphere/json}/generic/DeriveToJSON.scala | 0 .../sphere/json}/generic/EnumerationInstances.scala | 2 +- .../sphere/json}/generic/JSONTypeSwitch.scala | 2 +- .../sphere/json}/generic/JsonAnnotationReader.scala | 0 .../{io.sphere.json => io/sphere/json}/generic/generic.scala | 0 11 files changed, 2 insertions(+), 2 deletions(-) rename json/json-core/src/main/scala-3/{io.sphere.json => io/sphere/json}/FromJSON.scala (100%) rename json/json-core/src/main/scala-3/{io.sphere.json => io/sphere/json}/JSON.scala (100%) rename json/json-core/src/main/scala-3/{io.sphere.json => io/sphere/json}/ToJSON.scala (100%) rename json/json-core/src/main/scala-3/{io.sphere.json => io/sphere/json}/generic/Annotations.scala (100%) rename json/json-core/src/main/scala-3/{io.sphere.json => io/sphere/json}/generic/DeriveFromJSON.scala (100%) rename json/json-core/src/main/scala-3/{io.sphere.json => io/sphere/json}/generic/DeriveSingleton.scala (100%) rename json/json-core/src/main/scala-3/{io.sphere.json => io/sphere/json}/generic/DeriveToJSON.scala (100%) rename json/json-core/src/main/scala-3/{io.sphere.json => io/sphere/json}/generic/EnumerationInstances.scala (100%) rename json/json-core/src/main/scala-3/{io.sphere.json => io/sphere/json}/generic/JSONTypeSwitch.scala (100%) rename json/json-core/src/main/scala-3/{io.sphere.json => io/sphere/json}/generic/JsonAnnotationReader.scala (100%) rename json/json-core/src/main/scala-3/{io.sphere.json => io/sphere/json}/generic/generic.scala (100%) diff --git a/json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/FromJSON.scala similarity index 100% rename from json/json-core/src/main/scala-3/io.sphere.json/FromJSON.scala rename to json/json-core/src/main/scala-3/io/sphere/json/FromJSON.scala diff --git a/json/json-core/src/main/scala-3/io.sphere.json/JSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala similarity index 100% rename from json/json-core/src/main/scala-3/io.sphere.json/JSON.scala rename to json/json-core/src/main/scala-3/io/sphere/json/JSON.scala diff --git a/json/json-core/src/main/scala-3/io.sphere.json/ToJSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/ToJSON.scala similarity index 100% rename from json/json-core/src/main/scala-3/io.sphere.json/ToJSON.scala rename to json/json-core/src/main/scala-3/io/sphere/json/ToJSON.scala diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/Annotations.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/Annotations.scala similarity index 100% rename from json/json-core/src/main/scala-3/io.sphere.json/generic/Annotations.scala rename to json/json-core/src/main/scala-3/io/sphere/json/generic/Annotations.scala diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveFromJSON.scala similarity index 100% rename from json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveFromJSON.scala rename to json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveFromJSON.scala diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveSingleton.scala similarity index 100% rename from json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveSingleton.scala rename to json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveSingleton.scala diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveToJSON.scala similarity index 100% rename from json/json-core/src/main/scala-3/io.sphere.json/generic/DeriveToJSON.scala rename to json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveToJSON.scala diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/EnumerationInstances.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/EnumerationInstances.scala similarity index 100% rename from json/json-core/src/main/scala-3/io.sphere.json/generic/EnumerationInstances.scala rename to json/json-core/src/main/scala-3/io/sphere/json/generic/EnumerationInstances.scala index adb615db..83d9729b 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/EnumerationInstances.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/generic/EnumerationInstances.scala @@ -1,8 +1,8 @@ package io.sphere.json.generic -import org.json4s.JsonAST.* import cats.syntax.validated.* import io.sphere.json.* +import org.json4s.JsonAST.* import scala.collection.mutable diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/JSONTypeSwitch.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala similarity index 100% rename from json/json-core/src/main/scala-3/io.sphere.json/generic/JSONTypeSwitch.scala rename to json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala index 58297b95..b3cb6092 100644 --- a/json/json-core/src/main/scala-3/io.sphere.json/generic/JSONTypeSwitch.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala @@ -3,8 +3,8 @@ package io.sphere.json.generic import cats.data.Validated import io.sphere.json.{FromJSON, JSON, JSONParseError, ToJSON} import io.sphere.util.TraitMetaData -import org.json4s.{JObject, JString, jvalue2monadic, jvalue2readerSyntax} import org.json4s.DefaultJsonFormats.given +import org.json4s.{JObject, JString, jvalue2monadic, jvalue2readerSyntax} object JSONTypeSwitch { import scala.compiletime.{erasedValue, error, summonInline} diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/JsonAnnotationReader.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/JsonAnnotationReader.scala similarity index 100% rename from json/json-core/src/main/scala-3/io.sphere.json/generic/JsonAnnotationReader.scala rename to json/json-core/src/main/scala-3/io/sphere/json/generic/JsonAnnotationReader.scala diff --git a/json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/generic.scala similarity index 100% rename from json/json-core/src/main/scala-3/io.sphere.json/generic/generic.scala rename to json/json-core/src/main/scala-3/io/sphere/json/generic/generic.scala From da63668c283bee59668315d4a400865f18582eb9 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 23 May 2025 15:48:51 +0200 Subject: [PATCH 123/142] Add scala-2 syntax for 'jsonTypeSwitch' methods for scala3 --- .../io/sphere/json/generic/generic.scala | 65 +++++++++++++++ .../json/generic/JsonTypeSwitchSpec.scala | 83 +++++++++++-------- 2 files changed, 112 insertions(+), 36 deletions(-) diff --git a/json/json-core/src/main/scala-3/io/sphere/json/generic/generic.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/generic.scala index e691c2f7..781b8010 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/generic/generic.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/generic/generic.scala @@ -27,3 +27,68 @@ inline def fromJsonTypeSwitch[SuperType, SubTypes <: Tuple]: FromJSON[SuperType] val info = JSONTypeSwitch.readTraitInformation[SuperType, SubTypes] JSONTypeSwitch.fromJsonTypeSwitch[SuperType](info) } + +// Compatibility with the scala-2 methods, that will be deprecated later +// fromJsonTypeSwitch is not used, so no old syntax support will be added for now + +// jsonTypeSwitch is used up to 26 parameters, so I'll up to 28 +// format: off +inline def jsonTypeSwitch[SuperType, A1: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, Tuple1[A1]] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON, A21: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON, A21: JSON, A22: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON, A21: JSON, A22: JSON, A23: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22, A23)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON, A21: JSON, A22: JSON, A23: JSON, A24: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22, A23, A24)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON, A21: JSON, A22: JSON, A23: JSON, A24: JSON, A25: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22, A23, A24, A25)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON, A21: JSON, A22: JSON, A23: JSON, A24: JSON, A25: JSON, A26: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22, A23, A24, A25, A26)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON, A21: JSON, A22: JSON, A23: JSON, A24: JSON, A25: JSON, A26: JSON, A27: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22, A23, A24, A25, A26, A27)] +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON, A21: JSON, A22: JSON, A23: JSON, A24: JSON, A25: JSON, A26: JSON, A27: JSON, A28: JSON](ignoredList: List[Nothing]): JSON[SuperType] = + jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22, A23, A24, A25, A26, A27, A28)] +// format: on + +// toJsonTypeSwitch is used up to 5 parameters, so I'll add up to 7 diff --git a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala index a2b9b875..2ced702c 100644 --- a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala +++ b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala @@ -10,49 +10,55 @@ import org.json4s.* import org.json4s.DefaultReaders.StringReader class JsonTypeSwitchSpec extends AnyWordSpec with Matchers { + import JsonTypeSwitchSpec.* "jsonTypeSwitch" must { - import JsonTypeSwitchSpec.* - "derive a subset of a sealed trait" in { + { given JSON[B] = deriveJSON[B] - val format = jsonTypeSwitch[A, (B, C)] + "derive a subset of a sealed trait".withFormatters( + newSyntax = jsonTypeSwitch[A, (B, C)], + oldSyntax = jsonTypeSwitch[A, B, C](Nil) + ) { format => + val b = B(123) + val jsonB = format.write(b) - val b = B(123) - val jsonB = format.write(b) + val b2 = format.read(jsonB).getOrElse(null) - val b2 = format.read(jsonB).getOrElse(null) + b2 must be(b) - b2 must be(b) + val c = C(2345345) + val jsonC = format.write(c) - val c = C(2345345) - val jsonC = format.write(c) + val c2 = format.read(jsonC).getOrElse(null) - val c2 = format.read(jsonC).getOrElse(null) - - c2 must be(c) + c2 must be(c) + } } - "derive a subset of a sealed trait with a mongoKey" in { - val format = jsonTypeSwitch[A, (B, D)] - + "derive a subset of a sealed trait with a mongoKey".withFormatters( + newSyntax = jsonTypeSwitch[A, (B, D)], + oldSyntax = jsonTypeSwitch[A, B, D](Nil) + ) { format => val d = D(123) val json = format.write(d) val d2 = format.read(json) (json \ "type").as[String] must be("D2") d2 must be(Valid(d)) - } - "combine different sum types tree" in { + "combine different sum types tree".withFormatters( + newSyntax = jsonTypeSwitch[Message, (TypeA, TypeB)], + oldSyntax = jsonTypeSwitch[Message, (TypeA, TypeB)] + ) { format => val m: Seq[Message] = List( TypeA.ClassA1(23), TypeA.ClassA2("world"), TypeB.ClassB1(valid = false), TypeB.ClassB2(Seq("a23", "c62"))) - val jsons = m.map(Message.json.write) + val jsons = m.map(format.write) jsons must be( List( JObject("number" -> JLong(23), "type" -> JString("ClassA1")), @@ -63,34 +69,45 @@ class JsonTypeSwitchSpec extends AnyWordSpec with Matchers { "type" -> JString("ClassB2")) )) - val messages = jsons.map(Message.json.read).map(_.toOption.get) + val messages = jsons.map(format.read).map(_.toOption.get) messages must be(m) } - "handle custom implementations for subtypes" in { - + { given JSON[B] = new JSON[B] { override def read(jval: JValue): JValidation[B] = jval match { case JObject(List(_, "field" -> JString(s"Custom-B-${n}"))) => Valid(B(n.toInt)) case _ => ??? } + override def write(value: B): JValue = JObject(List("field" -> JString(s"Custom-B-${value.int}"))) } - val format = jsonTypeSwitch[A, (B, D, C)] - - List( - D(2345), - C(4), - B(34) - ).foreach { value => - val json = format.write(value) - format.read(json).getOrElse(null) must be(value) + + "handle custom implementations for subtypes".withFormatters( + newSyntax = jsonTypeSwitch[A, (B, D, C)], + oldSyntax = jsonTypeSwitch[A, B, D, C](Nil) + ) { format => + List(D(2345), C(4), B(34)).foreach { value => + val json = format.write(value) + format.read(json).getOrElse(null) must be(value) + } } } } + extension (string: String) { + def withFormatters[A](newSyntax: JSON[A], oldSyntax: JSON[A])(f: JSON[A] => Any): Unit = { + s"$string with newSyntax" in { + f(newSyntax) + } + + s"$string with oldSyntax" in { + f(oldSyntax) + } + } + } } object JsonTypeSwitchSpec { @@ -100,12 +117,6 @@ object JsonTypeSwitchSpec { @JSONTypeHint("D2") case class D(int: Int) extends A trait Message - object Message { - // this can be dangerous is the same class name is used in both sum types - // ex if we define TypeA.Class1 && TypeB.Class1 - // as both will use the same type value discriminator - implicit val json: JSON[Message] = jsonTypeSwitch[Message, (TypeA, TypeB)] - } sealed trait TypeA extends Message object TypeA { From e479bdf9fad67b787d6f36b257df0a33e1c98844 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Fri, 23 May 2025 16:10:28 +0200 Subject: [PATCH 124/142] test syntax improvements --- .../json/generic/JsonTypeSwitchSpec.scala | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala index 2ced702c..8b6b23c8 100644 --- a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala +++ b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala @@ -1,7 +1,7 @@ package io.sphere.json.generic import cats.data.Validated.Valid -import io.sphere.json.{JSON, JSONParseError, JValidation, deriveJSON} +import io.sphere.json.{JSON, JSONParseError, JValidation, deriveJSON, parseJSON} import io.sphere.json.generic.jsonTypeSwitch import org.json4s.JsonAST.JObject import org.scalatest.matchers.must.Matchers @@ -19,18 +19,18 @@ class JsonTypeSwitchSpec extends AnyWordSpec with Matchers { "derive a subset of a sealed trait".withFormatters( newSyntax = jsonTypeSwitch[A, (B, C)], oldSyntax = jsonTypeSwitch[A, B, C](Nil) - ) { format => + ) { val b = B(123) - val jsonB = format.write(b) + val jsonB = JSON[A].write(b) - val b2 = format.read(jsonB).getOrElse(null) + val b2 = JSON[A].read(jsonB).getOrElse(null) b2 must be(b) val c = C(2345345) - val jsonC = format.write(c) + val jsonC = JSON[A].write(c) - val c2 = format.read(jsonC).getOrElse(null) + val c2 = JSON[A].read(jsonC).getOrElse(null) c2 must be(c) } @@ -39,10 +39,10 @@ class JsonTypeSwitchSpec extends AnyWordSpec with Matchers { "derive a subset of a sealed trait with a mongoKey".withFormatters( newSyntax = jsonTypeSwitch[A, (B, D)], oldSyntax = jsonTypeSwitch[A, B, D](Nil) - ) { format => + ) { val d = D(123) - val json = format.write(d) - val d2 = format.read(json) + val json = JSON[A].write(d) + val d2 = JSON[A].read(json) (json \ "type").as[String] must be("D2") d2 must be(Valid(d)) @@ -51,14 +51,14 @@ class JsonTypeSwitchSpec extends AnyWordSpec with Matchers { "combine different sum types tree".withFormatters( newSyntax = jsonTypeSwitch[Message, (TypeA, TypeB)], oldSyntax = jsonTypeSwitch[Message, (TypeA, TypeB)] - ) { format => + ) { val m: Seq[Message] = List( TypeA.ClassA1(23), TypeA.ClassA2("world"), TypeB.ClassB1(valid = false), TypeB.ClassB2(Seq("a23", "c62"))) - val jsons = m.map(format.write) + val jsons = m.map(JSON[Message].write) jsons must be( List( JObject("number" -> JLong(23), "type" -> JString("ClassA1")), @@ -69,7 +69,7 @@ class JsonTypeSwitchSpec extends AnyWordSpec with Matchers { "type" -> JString("ClassB2")) )) - val messages = jsons.map(format.read).map(_.toOption.get) + val messages = jsons.map(JSON[Message].read).map(_.toOption.get) messages must be(m) } @@ -88,23 +88,30 @@ class JsonTypeSwitchSpec extends AnyWordSpec with Matchers { "handle custom implementations for subtypes".withFormatters( newSyntax = jsonTypeSwitch[A, (B, D, C)], oldSyntax = jsonTypeSwitch[A, B, D, C](Nil) - ) { format => - List(D(2345), C(4), B(34)).foreach { value => - val json = format.write(value) - format.read(json).getOrElse(null) must be(value) - } + ) { + check[A](D(2345), """ {"type": "D2", "int": 2345 } """) + check[A](C(4), """ {"type": "C", "int": 4 } """) + check[A](B(34), """ {"type": "B", "field": "Custom-B-34" } """) } } } + def check[A](a: A, json: String)(using format: JSON[A]): Unit = { + val parsedJson = parseJSON(json).getOrElse(null) + val json2 = format.write(a) + json2 must be(parsedJson) + format.read(json2).getOrElse(null) must be(a) + } + + type FormatTest[A] = JSON[A] ?=> Any extension (string: String) { - def withFormatters[A](newSyntax: JSON[A], oldSyntax: JSON[A])(f: JSON[A] => Any): Unit = { + def withFormatters[A](newSyntax: JSON[A], oldSyntax: JSON[A])(f: FormatTest[A]): Unit = { s"$string with newSyntax" in { - f(newSyntax) + f(using newSyntax) } s"$string with oldSyntax" in { - f(oldSyntax) + f(using oldSyntax) } } } From 37765be155b497c5d60645ecf753d38f1ce6aad6 Mon Sep 17 00:00:00 2001 From: benkobalog Date: Fri, 20 Jun 2025 10:16:49 +0200 Subject: [PATCH 125/142] Fix merge --- .../src/main/scala/io/sphere/json/ToJSONInstances.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/json/json-core/src/main/scala/io/sphere/json/ToJSONInstances.scala b/json/json-core/src/main/scala/io/sphere/json/ToJSONInstances.scala index f97ea5ac..7b50e6e5 100644 --- a/json/json-core/src/main/scala/io/sphere/json/ToJSONInstances.scala +++ b/json/json-core/src/main/scala/io/sphere/json/ToJSONInstances.scala @@ -3,6 +3,7 @@ package io.sphere.json import cats.data.NonEmptyList import java.util.{Currency, Locale, UUID} +import java.time import io.sphere.util.{BaseMoney, DateTimeFormats, HighPrecisionMoney, Money} import org.json4s.JsonAST._ import org.joda.time.DateTime From 2b04357ef7bca653c25a8f0d3ab4e81d9f0eaf61 Mon Sep 17 00:00:00 2001 From: benkobalog Date: Thu, 26 Jun 2025 16:49:15 +0200 Subject: [PATCH 126/142] change names --- build.sbt | 2 +- .../sphere/json/generic/DeriveSingleton.scala | 2 +- .../sphere/json/generic/JSONTypeSwitch.scala | 50 ++++++++----------- .../io/sphere/mongo/generic/generic.scala | 2 +- util/src/main/scala-3/AnnotationReader.scala | 2 +- 5 files changed, 25 insertions(+), 33 deletions(-) diff --git a/build.sbt b/build.sbt index 1d6d9128..41f85f98 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ lazy val scala3 = "3.3.5" // sbt-github-actions needs configuration in `ThisBuild` ThisBuild / crossScalaVersions := Seq(scala213, scala3) -ThisBuild / scalaVersion := scala213 +ThisBuild / scalaVersion := scala3 ThisBuild / githubWorkflowPublishTargetBranches := List() ThisBuild / githubWorkflowJavaVersions := List(JavaSpec.temurin("21")) ThisBuild / githubWorkflowBuildPreamble ++= List( diff --git a/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveSingleton.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveSingleton.scala index a50f4eb3..68c2c507 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveSingleton.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveSingleton.scala @@ -37,7 +37,7 @@ object DeriveSingleton { inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): DeriveSingleton[A] = { val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] - val typeHintMap = traitMetaData.subTypeFieldRenames + val typeHintMap = traitMetaData.subTypeSerializedTypeNames val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) val jsons: Seq[DeriveSingleton[Any]] = summonFormatters[mirrorOfSum.MirroredElemTypes] diff --git a/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala index b3cb6092..ab59df60 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala @@ -10,60 +10,52 @@ object JSONTypeSwitch { import scala.compiletime.{erasedValue, error, summonInline} case class TraitInformation( - mergedTypeHintMap: Map[String, String], - traitInTraitFormatterMap: Map[String, JSON[Any]], - caseClassFormatterMap: Map[String, JSON[Any]], + serializedTypeNames: Map[String, String], + traitFormatters: Map[String, JSON[Any]], + caseClassFormatters: Map[String, JSON[Any]], traitMetaData: TraitMetaData) inline def readTraitInformation[SuperType, SubTypes <: Tuple]: TraitInformation = { val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] - val formattersAndMetaData: Vector[(TraitMetaData, JSON[Any])] = summonFormatters[SubTypes]() + val formattersAndMetaData = summonFormatters[SubTypes]() - // - Separate Trait formatters from CaseClass formatters, so we can avoid adding the typeDiscriminator twice - // - Currently we support 2 layers of traits, because the AnnotationReader only tries to read to 2 levels val (traitInTraitInfo, caseClassFormatters) = formattersAndMetaData.partitionMap { (meta, formatter) => if (meta.isTrait) { val formatterByName = meta.subtypes.map((fieldName, m) => m.scalaName -> formatter) - Left(formatterByName -> meta.subTypeFieldRenames) - } else { - Right(meta.top.scalaName -> formatter) - } + Left((formatterByName, meta.subTypeSerializedTypeNames)) + } else + Right((meta.top.scalaName, formatter)) } - val (traitInTraitFormatters, traitInTraitRenames) = traitInTraitInfo.unzip - val traitInTraitFormatterMap = traitInTraitFormatters.fold(Map.empty)(_ ++ _) + val (subTraitFormatters, subTraitSerializedTypeNames) = traitInTraitInfo.unzip + // We could add some checks here to avoid the same type name in trait hierarchies + val traitFormatters = subTraitFormatters.fold(Map.empty)(_ ++ _) + val serializedTypeNames = + traitMetaData.subTypeSerializedTypeNames ++ subTraitSerializedTypeNames.fold(Map.empty)( + _ ++ _) - val caseClassFormatterMap = caseClassFormatters.toMap - - val mergedTypeHintMap = - traitMetaData.subTypeFieldRenames ++ traitInTraitRenames.fold(Map.empty)(_ ++ _) - - TraitInformation( - mergedTypeHintMap, - traitInTraitFormatterMap, - caseClassFormatterMap, - traitMetaData) + TraitInformation(serializedTypeNames, traitFormatters, caseClassFormatters.toMap, traitMetaData) } inline def toJsonTypeSwitch[SuperType](info: TraitInformation): ToJSON[SuperType] = ToJSON.instance { a => val scalaTypeName = a.asInstanceOf[Product].productPrefix - val serializedTypeName = info.mergedTypeHintMap.getOrElse(scalaTypeName, scalaTypeName) - val traitFormatterOpt = info.traitInTraitFormatterMap.get(scalaTypeName) + val serializedTypeName = info.serializedTypeNames.getOrElse(scalaTypeName, scalaTypeName) + val traitFormatterOpt = info.traitFormatters.get(scalaTypeName) traitFormatterOpt .map(_.write(a)) .getOrElse(writeCaseClass(info, scalaTypeName, a, serializedTypeName)) } inline def fromJsonTypeSwitch[SuperType](info: TraitInformation): FromJSON[SuperType] = { - val reverseTypeHintMap = info.mergedTypeHintMap.map((on, n) => (n, on)) - val allFormattersByTypeName = info.traitInTraitFormatterMap ++ info.caseClassFormatterMap + val scalaTypeNames = info.serializedTypeNames.map((on, n) => (n, on)) + val allFormattersByTypeName = info.traitFormatters ++ info.caseClassFormatters FromJSON.instance { case jObject: JObject => val serializedTypeName = (jObject \ info.traitMetaData.typeDiscriminator).as[String] - val scalaTypeName = reverseTypeHintMap.getOrElse(serializedTypeName, serializedTypeName) + val scalaTypeName = scalaTypeNames.getOrElse(serializedTypeName, serializedTypeName) allFormattersByTypeName(scalaTypeName).read(jObject).map(_.asInstanceOf[SuperType]) case x => Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$x'")) @@ -86,7 +78,7 @@ object JSONTypeSwitch { scalaTypeName: String, a: A, serializedTypeName: String): JObject = { - val jsonObj = info.caseClassFormatterMap(scalaTypeName).write(a) match { + val jsonObj = info.caseClassFormatters(scalaTypeName).write(a) match { case JObject(obj) => obj case json => throw new Exception(s"This code only handles objects as of now, but got: $json") @@ -102,7 +94,7 @@ object JSONTypeSwitch { case _: (t *: ts) => val traitMetaData = AnnotationReader.readTraitMetaData[t] val headFormatter = summonInline[JSON[t]].asInstanceOf[JSON[Any]] - summonFormatters[ts](acc :+ (traitMetaData -> headFormatter)) + summonFormatters[ts](acc :+ (traitMetaData, headFormatter)) } } diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala index 03628573..0c1c9b38 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala @@ -18,7 +18,7 @@ def mongoEnum(e: Enumeration): MongoFormat[e.Value] = new MongoFormat[e.Value] { inline def mongoTypeSwitch[SuperType, SubTypeTuple <: Tuple]: MongoFormat[SuperType] = { val traitMetaData = MongoAnnotationReader.readTraitMetaData[SuperType] - val typeHintMap = traitMetaData.subTypeFieldRenames + val typeHintMap = traitMetaData.subTypeSerializedTypeNames val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) val formatters = summonFormatters[SubTypeTuple]() val subTypeNames = summonMetaData[SubTypeTuple]() diff --git a/util/src/main/scala-3/AnnotationReader.scala b/util/src/main/scala-3/AnnotationReader.scala index 35db2952..01c4f1ff 100644 --- a/util/src/main/scala-3/AnnotationReader.scala +++ b/util/src/main/scala-3/AnnotationReader.scala @@ -34,7 +34,7 @@ case class TraitMetaData( val typeDiscriminator: String = typeHintFieldRaw.getOrElse(defaultTypeDiscriminatorName) - val subTypeFieldRenames: Map[String, String] = subtypes.collect { + val subTypeSerializedTypeNames: Map[String, String] = subtypes.collect { case (scalaName, classMeta) if classMeta.typeHint.isDefined => scalaName -> classMeta.typeHint.get } From cd8b3315dd939aa6cea83bd2ecee4412cf62d6f7 Mon Sep 17 00:00:00 2001 From: benkobalog Date: Thu, 26 Jun 2025 17:29:08 +0200 Subject: [PATCH 127/142] Syntax changes --- .../main/scala-3/io/sphere/json/JSON.scala | 10 ++- .../sphere/json/generic/JSONTypeSwitch.scala | 14 ++-- .../io/sphere/json/generic/JSONSpec.scala | 48 +++++++------- .../json/generic/JsonTypeSwitchSpec.scala | 65 +++++++++++++++++++ util/src/main/scala-3/AnnotationReader.scala | 5 +- 5 files changed, 108 insertions(+), 34 deletions(-) diff --git a/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala index 94924995..fb9e705b 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala @@ -5,9 +5,15 @@ import org.json4s.JsonAST.JValue import scala.deriving.Mirror -trait JSON[A] extends FromJSON[A] with ToJSON[A] +trait JSON[A] extends FromJSON[A] with ToJSON[A] { + // This field is only used in case we derive a trait, for classes/objects it remains empty + // It uses the JSON names not the Scala names (if there's @JSONTypeHint renaming a class the renamed name is used here) + def subTypeNames: List[String] = Nil +} inline def deriveJSON[A](using Mirror.Of[A]): JSON[A] = JSON.derived +inline def deriveToJSON[A](using Mirror.Of[A]): ToJSON[A] = ToJSON.derived +inline def deriveFromJSON[A](using Mirror.Of[A]): FromJSON[A] = FromJSON.derived object JSON extends JSONCatsInstances { inline def apply[A: JSON]: JSON[A] = summon[JSON[A]] @@ -16,11 +22,13 @@ object JSON extends JSONCatsInstances { def instance[A]( readFn: JValue => JValidation[A], writeFn: A => JValue, + subTypeNameList: List[String] = Nil, fieldSet: Set[String] = FromJSON.emptyFieldsSet): JSON[A] = new { override def read(jval: JValue): JValidation[A] = readFn(jval) override def write(value: A): JValue = writeFn(value) override val fields: Set[String] = fieldSet + override def subTypeNames: List[String] = subTypeNameList } private def instance[A](using fromJSON: FromJSON[A], toJSON: ToJSON[A]): JSON[A] = diff --git a/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala index ab59df60..afcb49ae 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala @@ -13,7 +13,8 @@ object JSONTypeSwitch { serializedTypeNames: Map[String, String], traitFormatters: Map[String, JSON[Any]], caseClassFormatters: Map[String, JSON[Any]], - traitMetaData: TraitMetaData) + traitMetaData: TraitMetaData + ) inline def readTraitInformation[SuperType, SubTypes <: Tuple]: TraitInformation = { val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] @@ -32,8 +33,8 @@ object JSONTypeSwitch { // We could add some checks here to avoid the same type name in trait hierarchies val traitFormatters = subTraitFormatters.fold(Map.empty)(_ ++ _) val serializedTypeNames = - traitMetaData.subTypeSerializedTypeNames ++ subTraitSerializedTypeNames.fold(Map.empty)( - _ ++ _) + subTraitSerializedTypeNames.fold(Map.empty)( + _ ++ _) ++ traitMetaData.subTypeSerializedTypeNames TraitInformation(serializedTypeNames, traitFormatters, caseClassFormatters.toMap, traitMetaData) } @@ -41,7 +42,7 @@ object JSONTypeSwitch { inline def toJsonTypeSwitch[SuperType](info: TraitInformation): ToJSON[SuperType] = ToJSON.instance { a => val scalaTypeName = a.asInstanceOf[Product].productPrefix - val serializedTypeName = info.serializedTypeNames.getOrElse(scalaTypeName, scalaTypeName) + val serializedTypeName = info.serializedTypeNames(scalaTypeName) val traitFormatterOpt = info.traitFormatters.get(scalaTypeName) traitFormatterOpt .map(_.write(a)) @@ -55,7 +56,7 @@ object JSONTypeSwitch { FromJSON.instance { case jObject: JObject => val serializedTypeName = (jObject \ info.traitMetaData.typeDiscriminator).as[String] - val scalaTypeName = scalaTypeNames.getOrElse(serializedTypeName, serializedTypeName) + val scalaTypeName = scalaTypeNames(serializedTypeName) allFormattersByTypeName(scalaTypeName).read(jObject).map(_.asInstanceOf[SuperType]) case x => Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$x'")) @@ -69,7 +70,8 @@ object JSONTypeSwitch { JSON.instance( writeFn = toJson.write, - readFn = fromJson.read + readFn = fromJson.read, + subTypeNameList = info.serializedTypeNames.values.toList ) } diff --git a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala index 236feb38..92eda8a4 100644 --- a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala +++ b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JSONSpec.scala @@ -232,14 +232,14 @@ class JSONSpec extends AnyFunSpec with Matchers { describe("ToJSON and FromJSON") { it("must provide derived JSON instances for sum types") { // ToJSON - given ToJSON[Bird] = ToJSON.derived[Bird] - given ToJSON[Dog] = ToJSON.derived[Dog] - given ToJSON[Cat] = ToJSON.derived[Cat] + given ToJSON[Bird] = deriveToJSON[Bird] + given ToJSON[Dog] = deriveToJSON[Dog] + given ToJSON[Cat] = deriveToJSON[Cat] given ToJSON[Animal] = toJsonTypeSwitch[Animal, (Bird, Dog, Cat)] // FromJSON - given FromJSON[Bird] = FromJSON.derived[Bird] - given FromJSON[Dog] = FromJSON.derived[Dog] - given FromJSON[Cat] = FromJSON.derived[Cat] + given FromJSON[Bird] = deriveFromJSON[Bird] + given FromJSON[Dog] = deriveFromJSON[Dog] + given FromJSON[Cat] = deriveFromJSON[Cat] given FromJSON[Animal] = fromJsonTypeSwitch[Animal, (Bird, Dog, Cat)] List(Bird("Peewee"), Dog("Hasso"), Cat("Felidae")).foreach { (a: Animal) => @@ -248,20 +248,20 @@ class JSONSpec extends AnyFunSpec with Matchers { } it("must provide derived instances for product types with concrete type parameters") { - given ToJSON[GenericA[String]] = ToJSON.derived - given FromJSON[GenericA[String]] = FromJSON.derived + given ToJSON[GenericA[String]] = deriveToJSON + given FromJSON[GenericA[String]] = deriveFromJSON val a = GenericA("hello") fromJSON[GenericA[String]](toJSON(a)) must equal(Valid(a)) } it("must provide derived instances for sum types with a mix of case class / object") { // ToJSON - given ToJSON[SingletonMixed.type] = ToJSON.derived - given ToJSON[RecordMixed] = ToJSON.derived + given ToJSON[SingletonMixed.type] = deriveToJSON + given ToJSON[RecordMixed] = deriveToJSON given ToJSON[Mixed] = toJsonTypeSwitch[Mixed, (SingletonMixed.type, RecordMixed)] // FromJSON - given FromJSON[SingletonMixed.type] = FromJSON.derived - given FromJSON[RecordMixed] = FromJSON.derived + given FromJSON[SingletonMixed.type] = deriveFromJSON + given FromJSON[RecordMixed] = deriveFromJSON given FromJSON[Mixed] = fromJsonTypeSwitch[Mixed, (SingletonMixed.type, RecordMixed)] List(SingletonMixed, RecordMixed(1)).foreach { m => @@ -282,10 +282,10 @@ class JSONSpec extends AnyFunSpec with Matchers { it("must handle subclasses correctly in `jsonTypeSwitch`") { // ToJSON - given ToJSON[TestSubjectConcrete1] = ToJSON.derived - given ToJSON[TestSubjectConcrete2] = ToJSON.derived - given ToJSON[TestSubjectConcrete3] = ToJSON.derived - given ToJSON[TestSubjectConcrete4] = ToJSON.derived + given ToJSON[TestSubjectConcrete1] = deriveToJSON + given ToJSON[TestSubjectConcrete2] = deriveToJSON + given ToJSON[TestSubjectConcrete3] = deriveToJSON + given ToJSON[TestSubjectConcrete4] = deriveToJSON given ToJSON[TestSubjectCategoryA] = toJsonTypeSwitch[TestSubjectCategoryA, (TestSubjectConcrete1, TestSubjectConcrete2)] given ToJSON[TestSubjectCategoryB] = @@ -294,10 +294,10 @@ class JSONSpec extends AnyFunSpec with Matchers { toJsonTypeSwitch[TestSubjectBase, (TestSubjectCategoryA, TestSubjectCategoryB)] // FromJSON - given FromJSON[TestSubjectConcrete1] = FromJSON.derived - given FromJSON[TestSubjectConcrete2] = FromJSON.derived - given FromJSON[TestSubjectConcrete3] = FromJSON.derived - given FromJSON[TestSubjectConcrete4] = FromJSON.derived + given FromJSON[TestSubjectConcrete1] = deriveFromJSON + given FromJSON[TestSubjectConcrete2] = deriveFromJSON + given FromJSON[TestSubjectConcrete3] = deriveFromJSON + given FromJSON[TestSubjectConcrete4] = deriveFromJSON given FromJSON[TestSubjectCategoryA] = fromJsonTypeSwitch[TestSubjectCategoryA, (TestSubjectConcrete1, TestSubjectConcrete2)] given FromJSON[TestSubjectCategoryB] = @@ -323,10 +323,10 @@ class JSONSpec extends AnyFunSpec with Matchers { it("must provide derived JSON instances for product types (case classes)") { import JSONSpec.{Milestone, Project} - given ToJSON[Milestone] = ToJSON.derived[Milestone] - given ToJSON[Project] = ToJSON.derived[Project] - given FromJSON[Milestone] = FromJSON.derived[Milestone] - given FromJSON[Project] = FromJSON.derived[Project] + given ToJSON[Milestone] = deriveToJSON[Milestone] + given ToJSON[Project] = deriveToJSON[Project] + given FromJSON[Milestone] = deriveFromJSON[Milestone] + given FromJSON[Project] = deriveFromJSON[Project] val proj = Project(42, "Linux", 7, Milestone("1.0") :: Milestone("2.0") :: Milestone("3.0") :: Nil) diff --git a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala index 8b6b23c8..fad648fb 100644 --- a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala +++ b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala @@ -1,6 +1,7 @@ package io.sphere.json.generic import cats.data.Validated.Valid +import cats.implicits.toTraverseOps import io.sphere.json.{JSON, JSONParseError, JValidation, deriveJSON, parseJSON} import io.sphere.json.generic.jsonTypeSwitch import org.json4s.JsonAST.JObject @@ -94,6 +95,28 @@ class JsonTypeSwitchSpec extends AnyWordSpec with Matchers { check[A](B(34), """ {"type": "B", "field": "Custom-B-34" } """) } } + + "handle PlatformFormattedNotification case" in { + + type Trait234 = SubTrait2 *: (SubTrait3, SubTrait4) + + val formatSuper: JSON[SuperTrait] = jsonTypeSwitch[SuperTrait, SubTrait1 *: Trait234] + + val objs = + List[SuperTrait]( + SubTrait1.O1, + SubTrait1.O2, + SubTrait2.O3, + SubTrait2.O4, + SubTrait3.O5, + SubTrait3.O6, + SubTrait4.O7) + + val res = objs.map(formatSuper.write).map(formatSuper.read).sequence.getOrElse(null) + + res must be(objs) + + } } def check[A](a: A, json: String)(using format: JSON[A]): Unit = { @@ -138,4 +161,46 @@ object JsonTypeSwitchSpec { case class ClassB2(references: Seq[String]) extends TypeB implicit val json: JSON[TypeB] = deriveJSON[TypeB] } + + trait SuperTrait + + sealed trait SubTrait1 extends SuperTrait + + object SubTrait1 { + case object O1 extends SubTrait1 + + case object O2 extends SubTrait1 + + given JSON[SubTrait1] = deriveJSON + } + + sealed trait SubTrait2 extends SuperTrait + + object SubTrait2 { + case object O3 extends SubTrait2 + + case object O4 extends SubTrait2 + + given JSON[SubTrait2] = deriveJSON + } + + sealed trait SubTrait3 extends SuperTrait + + object SubTrait3 { + case object O5 extends SubTrait3 + + case object O6 extends SubTrait3 + + given JSON[SubTrait3] = deriveJSON + } + + sealed trait SubTrait4 extends SuperTrait + + object SubTrait4 { + case object O7 extends SubTrait4 + + case object O8 extends SubTrait4 + + given JSON[SubTrait4] = deriveJSON + } } diff --git a/util/src/main/scala-3/AnnotationReader.scala b/util/src/main/scala-3/AnnotationReader.scala index 01c4f1ff..30f5dfff 100644 --- a/util/src/main/scala-3/AnnotationReader.scala +++ b/util/src/main/scala-3/AnnotationReader.scala @@ -34,9 +34,8 @@ case class TraitMetaData( val typeDiscriminator: String = typeHintFieldRaw.getOrElse(defaultTypeDiscriminatorName) - val subTypeSerializedTypeNames: Map[String, String] = subtypes.collect { - case (scalaName, classMeta) if classMeta.typeHint.isDefined => - scalaName -> classMeta.typeHint.get + val subTypeSerializedTypeNames: Map[String, String] = subtypes.map { + case (scalaName, classMeta) => scalaName -> classMeta.typeHint.getOrElse(scalaName) } } From d5ea1700db5139e094af49f7c9f69ca7ae676da8 Mon Sep 17 00:00:00 2001 From: benkobalog Date: Thu, 26 Jun 2025 17:29:35 +0200 Subject: [PATCH 128/142] set sbt scala version back --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 41f85f98..1d6d9128 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ lazy val scala3 = "3.3.5" // sbt-github-actions needs configuration in `ThisBuild` ThisBuild / crossScalaVersions := Seq(scala213, scala3) -ThisBuild / scalaVersion := scala3 +ThisBuild / scalaVersion := scala213 ThisBuild / githubWorkflowPublishTargetBranches := List() ThisBuild / githubWorkflowJavaVersions := List(JavaSpec.temurin("21")) ThisBuild / githubWorkflowBuildPreamble ++= List( From e903fd45f02649dc8c16b82279dc91b7cf9991e7 Mon Sep 17 00:00:00 2001 From: benkobalog Date: Mon, 30 Jun 2025 10:28:47 +0200 Subject: [PATCH 129/142] Refactor JsonTypeSwitch so it can be "merged" with other json instances to allow compatibility with the scala 2 API --- .../scala-3/io/sphere/json/FromJSON.scala | 6 + .../main/scala-3/io/sphere/json/JSON.scala | 24 +-- .../main/scala-3/io/sphere/json/ToJSON.scala | 10 +- .../sphere/json/generic/DeriveFromJSON.scala | 5 +- .../io/sphere/json/generic/DeriveToJSON.scala | 4 +- .../sphere/json/generic/JSONTypeSwitch.scala | 163 +++++++++++------- .../io/sphere/json/generic/generic.scala | 8 +- .../scala/io/sphere/json/ToJSONSpec.scala | 2 +- .../json/generic/JsonTypeSwitchSpec.scala | 160 ++++++++--------- 9 files changed, 213 insertions(+), 169 deletions(-) diff --git a/json/json-core/src/main/scala-3/io/sphere/json/FromJSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/FromJSON.scala index 1f0b4a5d..f8ddbad3 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/FromJSON.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/FromJSON.scala @@ -1,6 +1,7 @@ package io.sphere.json import io.sphere.json.JValidation +import io.sphere.json.generic.JSONTypeSwitch.Formatters import org.json4s.JsonAST.JValue /** Type class for types that can be read from JSON. */ @@ -10,6 +11,9 @@ trait FromJSON[A] extends Serializable { /** needed JSON fields - ignored if empty */ val fields: Set[String] = FromJSON.emptyFieldsSet + + // This is automatically filled for traits + val fromFormatters: Formatters[FromJSON] = null } object FromJSON extends FromJSONInstances with FromJSONCatsInstances with generic.DeriveFromJSON { @@ -19,9 +23,11 @@ object FromJSON extends FromJSONInstances with FromJSONCatsInstances with generi def instance[A]( readFn: JValue => JValidation[A], + fromFs: Formatters[FromJSON], fieldSet: Set[String] = emptyFieldsSet): FromJSON[A] = new { override def read(jval: JValue): JValidation[A] = readFn(jval) override val fields: Set[String] = fieldSet + override val fromFormatters: Formatters[FromJSON] = fromFs } } diff --git a/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala index fb9e705b..087c2363 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala @@ -1,6 +1,7 @@ package io.sphere.json import cats.implicits.* +import io.sphere.json.generic.JSONTypeSwitch.Formatters import org.json4s.JsonAST.JValue import scala.deriving.Mirror @@ -17,28 +18,29 @@ inline def deriveFromJSON[A](using Mirror.Of[A]): FromJSON[A] = FromJSON.derived object JSON extends JSONCatsInstances { inline def apply[A: JSON]: JSON[A] = summon[JSON[A]] - inline given derived[A](using fromJSON: FromJSON[A], toJSON: ToJSON[A]): JSON[A] = instance + inline given derived[A](using fromJSON: FromJSON[A], toJSON: ToJSON[A]): JSON[A] = + instance( + readFn = fromJSON.read, + writeFn = toJSON.write, + fromFs = fromJSON.fromFormatters, + toFs = toJSON.toFormatters, + fieldSet = fromJSON.fields + ) def instance[A]( readFn: JValue => JValidation[A], writeFn: A => JValue, + fromFs: Formatters[FromJSON], + toFs: Formatters[ToJSON], subTypeNameList: List[String] = Nil, fieldSet: Set[String] = FromJSON.emptyFieldsSet): JSON[A] = new { - override def read(jval: JValue): JValidation[A] = readFn(jval) override def write(value: A): JValue = writeFn(value) override val fields: Set[String] = fieldSet override def subTypeNames: List[String] = subTypeNameList + override val fromFormatters: Formatters[FromJSON] = fromFs + override val toFormatters: Formatters[ToJSON] = toFs } - - private def instance[A](using fromJSON: FromJSON[A], toJSON: ToJSON[A]): JSON[A] = - new JSON[A] { - override def read(jval: JValue): JValidation[A] = fromJSON.read(jval) - - override def write(value: A): JValue = toJSON.write(value) - - override val fields: Set[String] = fromJSON.fields - } } class JSONException(msg: String) extends RuntimeException(msg) diff --git a/json/json-core/src/main/scala-3/io/sphere/json/ToJSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/ToJSON.scala index 26123236..96870ed9 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/ToJSON.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/ToJSON.scala @@ -1,13 +1,14 @@ package io.sphere.json +import io.sphere.json.generic.JSONTypeSwitch.Formatters import org.json4s.JsonAST.JValue -import java.time -import java.util.{Currency, Locale, UUID} - /** Type class for types that can be written to JSON. */ trait ToJSON[A] extends Serializable { def write(value: A): JValue + + // Filled automatically for traits + val toFormatters: Formatters[ToJSON] = null } class JSONWriteException(msg: String) extends JSONException(msg) @@ -18,7 +19,8 @@ object ToJSON extends ToJSONInstances with ToJSONCatsInstances with generic.Deri /** construct an instance from a function */ - def instance[T](toJson: T => JValue): ToJSON[T] = new ToJSON[T] { + def instance[T](toFs: Formatters[ToJSON])(toJson: T => JValue): ToJSON[T] = new ToJSON[T] { override def write(value: T): JValue = toJson(value) + override val toFormatters: Formatters[ToJSON] = toFs } } diff --git a/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveFromJSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveFromJSON.scala index db65d1b8..f2a8d4fc 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveFromJSON.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveFromJSON.scala @@ -13,7 +13,7 @@ trait DeriveFromJSON { protected object Derivation { - import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} + import scala.compiletime.{erasedValue, summonInline} inline def derived[A](using m: Mirror.Of[A]): FromJSON[A] = inline m match { @@ -49,7 +49,8 @@ trait DeriveFromJSON { case x => Validated.invalidNel(JSONParseError(s"JSON object expected. $x")) }, - fieldSet = fieldNames.toSet + fieldSet = fieldNames.toSet, + fromFs = null, ) } diff --git a/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveToJSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveToJSON.scala index 161a12b8..581c3fcb 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveToJSON.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveToJSON.scala @@ -12,7 +12,7 @@ trait DeriveToJSON { protected object Derivation { - import scala.compiletime.{constValue, constValueTuple, erasedValue, summonInline} + import scala.compiletime.{erasedValue, summonInline} inline def derived[A](using m: Mirror.Of[A]): ToJSON[A] = inline m match { @@ -24,7 +24,7 @@ trait DeriveToJSON { val caseClassMetaData: TypeMetaData = AnnotationReader.readTypeMetaData[A] val toJsons: Vector[ToJSON[Any]] = summonToJson[mirrorOfProduct.MirroredElemTypes] - ToJSON.instance { value => + ToJSON.instance(null) { value => val caseClassFields = value.asInstanceOf[Product].productIterator toJsons.iterator .zip(caseClassFields) diff --git a/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala index afcb49ae..48bbe1a0 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala @@ -2,101 +2,134 @@ package io.sphere.json.generic import cats.data.Validated import io.sphere.json.{FromJSON, JSON, JSONParseError, ToJSON} -import io.sphere.util.TraitMetaData import org.json4s.DefaultJsonFormats.given import org.json4s.{JObject, JString, jvalue2monadic, jvalue2readerSyntax} +import scala.compiletime.{constValue, constValueTuple} + object JSONTypeSwitch { - import scala.compiletime.{erasedValue, error, summonInline} + import scala.compiletime.{erasedValue, summonInline} - case class TraitInformation( + case class Formatters[JsonF[_]]( serializedTypeNames: Map[String, String], - traitFormatters: Map[String, JSON[Any]], - caseClassFormatters: Map[String, JSON[Any]], - traitMetaData: TraitMetaData - ) + forCaseClasses: Map[String, JsonF[Any]], + typeDiscriminator: String + ) { + def addTypeNames(names: Map[String, String]): Formatters[JsonF] = + copy(serializedTypeNames = serializedTypeNames ++ names) + } - inline def readTraitInformation[SuperType, SubTypes <: Tuple]: TraitInformation = { - val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] - val formattersAndMetaData = summonFormatters[SubTypes]() - - val (traitInTraitInfo, caseClassFormatters) = - formattersAndMetaData.partitionMap { (meta, formatter) => - if (meta.isTrait) { - val formatterByName = meta.subtypes.map((fieldName, m) => m.scalaName -> formatter) - Left((formatterByName, meta.subTypeSerializedTypeNames)) - } else - Right((meta.top.scalaName, formatter)) - } + object Formatters { + def merge[JsonF[_]](f1: Formatters[JsonF], f2: Formatters[JsonF]): Formatters[JsonF] = + Formatters[JsonF]( + serializedTypeNames = f1.serializedTypeNames ++ f2.serializedTypeNames, + forCaseClasses = f1.forCaseClasses ++ f2.forCaseClasses, + typeDiscriminator = f1.typeDiscriminator + ) + } - val (subTraitFormatters, subTraitSerializedTypeNames) = traitInTraitInfo.unzip - // We could add some checks here to avoid the same type name in trait hierarchies - val traitFormatters = subTraitFormatters.fold(Map.empty)(_ ++ _) - val serializedTypeNames = - subTraitSerializedTypeNames.fold(Map.empty)( - _ ++ _) ++ traitMetaData.subTypeSerializedTypeNames + inline def deriveToFormatters[SuperType, SubTypes <: Tuple]: Formatters[ToJSON] = { + val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] + summonToFormatters[SubTypes]() + .reduce(Formatters.merge) + .copy(typeDiscriminator = traitMetaData.typeDiscriminator) + .addTypeNames(traitMetaData.subTypeSerializedTypeNames) + } - TraitInformation(serializedTypeNames, traitFormatters, caseClassFormatters.toMap, traitMetaData) + inline def deriveFromFormatters[SuperType, SubTypes <: Tuple]: Formatters[FromJSON] = { + val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] + summonFromFormatters[SubTypes]() + .reduce(Formatters.merge) + .copy(typeDiscriminator = traitMetaData.typeDiscriminator) + .addTypeNames(traitMetaData.subTypeSerializedTypeNames) } - inline def toJsonTypeSwitch[SuperType](info: TraitInformation): ToJSON[SuperType] = - ToJSON.instance { a => + inline def toJsonTypeSwitch[SuperType](formatters: Formatters[ToJSON]): ToJSON[SuperType] = + ToJSON.instance(formatters) { a => val scalaTypeName = a.asInstanceOf[Product].productPrefix - val serializedTypeName = info.serializedTypeNames(scalaTypeName) - val traitFormatterOpt = info.traitFormatters.get(scalaTypeName) - traitFormatterOpt - .map(_.write(a)) - .getOrElse(writeCaseClass(info, scalaTypeName, a, serializedTypeName)) + val serializedTypeName = formatters.serializedTypeNames(scalaTypeName) + val jsonObj = formatters.forCaseClasses(scalaTypeName).write(a) match { + case JObject(obj) => obj + case json => + throw new Exception(s"This code only handles objects as of now, but got: $json") + } + val typeDiscriminator = formatters.typeDiscriminator -> JString(serializedTypeName) + JObject(typeDiscriminator :: jsonObj) } - inline def fromJsonTypeSwitch[SuperType](info: TraitInformation): FromJSON[SuperType] = { - val scalaTypeNames = info.serializedTypeNames.map((on, n) => (n, on)) - val allFormattersByTypeName = info.traitFormatters ++ info.caseClassFormatters - - FromJSON.instance { - case jObject: JObject => - val serializedTypeName = (jObject \ info.traitMetaData.typeDiscriminator).as[String] - val scalaTypeName = scalaTypeNames(serializedTypeName) - allFormattersByTypeName(scalaTypeName).read(jObject).map(_.asInstanceOf[SuperType]) - case x => - Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$x'")) - } + inline def fromJsonTypeSwitch[SuperType]( + formatters: Formatters[FromJSON]): FromJSON[SuperType] = { + val scalaTypeNames = formatters.serializedTypeNames.map((on, n) => (n, on)) + + FromJSON.instance( + readFn = { + case jObject: JObject => + val serializedTypeName = (jObject \ formatters.typeDiscriminator).as[String] + val scalaTypeName = scalaTypeNames(serializedTypeName) + formatters.forCaseClasses(scalaTypeName).read(jObject).map(_.asInstanceOf[SuperType]) + case x => + Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$x'")) + }, + fromFs = formatters + ) } inline def jsonTypeSwitch[SuperType, SubTypes <: Tuple]: JSON[SuperType] = { - val info = readTraitInformation[SuperType, SubTypes] - val fromJson = fromJsonTypeSwitch[SuperType](info) - val toJson = toJsonTypeSwitch[SuperType](info) + val fromFormatters = deriveFromFormatters[SuperType, SubTypes] + val fromJson = fromJsonTypeSwitch[SuperType](fromFormatters) + val toFormatters = deriveToFormatters[SuperType, SubTypes] + val toJson = toJsonTypeSwitch[SuperType](toFormatters) JSON.instance( writeFn = toJson.write, readFn = fromJson.read, - subTypeNameList = info.serializedTypeNames.values.toList + subTypeNameList = fromFormatters.serializedTypeNames.values.toList, + fromFs = fromJson.fromFormatters, + toFs = toJson.toFormatters ) } - private def writeCaseClass[A]( - info: TraitInformation, - scalaTypeName: String, - a: A, - serializedTypeName: String): JObject = { - val jsonObj = info.caseClassFormatters(scalaTypeName).write(a) match { - case JObject(obj) => obj - case json => - throw new Exception(s"This code only handles objects as of now, but got: $json") + inline private def summonFromFormatters[T <: Tuple]( + d: Int = 0, + acc: Vector[Formatters[FromJSON]] = Vector.empty): Vector[Formatters[FromJSON]] = + inline erasedValue[T] match { + case _: EmptyTuple => acc + case _: (t *: ts) => + val traitMetaData = AnnotationReader.readTraitMetaData[t] + val headFormatter = summonInline[FromJSON[t]].asInstanceOf[FromJSON[Any]] + val formatterMap = + if (traitMetaData.isTrait) { + headFormatter.fromFormatters.forCaseClasses + } else + Map(traitMetaData.top.scalaName -> headFormatter) + + val f = Formatters[FromJSON]( + serializedTypeNames = traitMetaData.subTypeSerializedTypeNames, + forCaseClasses = formatterMap, + typeDiscriminator = traitMetaData.typeDiscriminator + ) + summonFromFormatters[ts](d + 1, acc :+ f) } - val typeDiscriminator = info.traitMetaData.typeDiscriminator -> JString(serializedTypeName) - JObject(typeDiscriminator :: jsonObj) - } - inline private def summonFormatters[T <: Tuple]( - acc: Vector[(TraitMetaData, JSON[Any])] = Vector.empty): Vector[(TraitMetaData, JSON[Any])] = + inline private def summonToFormatters[T <: Tuple]( + acc: Vector[Formatters[ToJSON]] = Vector.empty): Vector[Formatters[ToJSON]] = inline erasedValue[T] match { case _: EmptyTuple => acc case _: (t *: ts) => val traitMetaData = AnnotationReader.readTraitMetaData[t] - val headFormatter = summonInline[JSON[t]].asInstanceOf[JSON[Any]] - summonFormatters[ts](acc :+ (traitMetaData, headFormatter)) + val headFormatter = summonInline[ToJSON[t]].asInstanceOf[ToJSON[Any]] + val formatterMap = + if (traitMetaData.isTrait) + headFormatter.toFormatters.forCaseClasses + else + Map(traitMetaData.top.scalaName -> headFormatter) + + val f = Formatters[ToJSON]( + serializedTypeNames = traitMetaData.subTypeSerializedTypeNames, + forCaseClasses = formatterMap, + typeDiscriminator = traitMetaData.typeDiscriminator + ) + summonToFormatters[ts](acc :+ f) } } diff --git a/json/json-core/src/main/scala-3/io/sphere/json/generic/generic.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/generic.scala index 781b8010..df774e76 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/generic/generic.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/generic/generic.scala @@ -19,13 +19,13 @@ inline def jsonTypeSwitch[SuperType, SubTypes <: Tuple]: JSON[SuperType] = JSONTypeSwitch.jsonTypeSwitch[SuperType, SubTypes] inline def toJsonTypeSwitch[SuperType, SubTypes <: Tuple]: ToJSON[SuperType] = { - val info = JSONTypeSwitch.readTraitInformation[SuperType, SubTypes] - JSONTypeSwitch.toJsonTypeSwitch[SuperType](info) + val f = JSONTypeSwitch.deriveToFormatters[SuperType, SubTypes] + JSONTypeSwitch.toJsonTypeSwitch[SuperType](f) } inline def fromJsonTypeSwitch[SuperType, SubTypes <: Tuple]: FromJSON[SuperType] = { - val info = JSONTypeSwitch.readTraitInformation[SuperType, SubTypes] - JSONTypeSwitch.fromJsonTypeSwitch[SuperType](info) + val f = JSONTypeSwitch.deriveFromFormatters[SuperType, SubTypes] + JSONTypeSwitch.fromJsonTypeSwitch[SuperType](f) } // Compatibility with the scala-2 methods, that will be deprecated later diff --git a/json/json-core/src/test/scala/io/sphere/json/ToJSONSpec.scala b/json/json-core/src/test/scala/io/sphere/json/ToJSONSpec.scala index acaeea18..6215e228 100644 --- a/json/json-core/src/test/scala/io/sphere/json/ToJSONSpec.scala +++ b/json/json-core/src/test/scala/io/sphere/json/ToJSONSpec.scala @@ -12,7 +12,7 @@ class ToJSONSpec extends AnyWordSpec with Matchers { "ToJSON.apply" must { "create a ToJSON" in { - implicit val encodeUser: ToJSON[User] = ToJSON.instance[User](u => + implicit val encodeUser: ToJSON[User] = ToJSON.instance[User](null)(u => JObject( List( "id" -> toJValue(u.id), diff --git a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala index fad648fb..f4f2f34f 100644 --- a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala +++ b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala @@ -15,86 +15,86 @@ class JsonTypeSwitchSpec extends AnyWordSpec with Matchers { "jsonTypeSwitch" must { - { - given JSON[B] = deriveJSON[B] - "derive a subset of a sealed trait".withFormatters( - newSyntax = jsonTypeSwitch[A, (B, C)], - oldSyntax = jsonTypeSwitch[A, B, C](Nil) - ) { - val b = B(123) - val jsonB = JSON[A].write(b) - - val b2 = JSON[A].read(jsonB).getOrElse(null) - - b2 must be(b) - - val c = C(2345345) - val jsonC = JSON[A].write(c) - - val c2 = JSON[A].read(jsonC).getOrElse(null) - - c2 must be(c) - } - } - - "derive a subset of a sealed trait with a mongoKey".withFormatters( - newSyntax = jsonTypeSwitch[A, (B, D)], - oldSyntax = jsonTypeSwitch[A, B, D](Nil) - ) { - val d = D(123) - val json = JSON[A].write(d) - val d2 = JSON[A].read(json) - - (json \ "type").as[String] must be("D2") - d2 must be(Valid(d)) - } - - "combine different sum types tree".withFormatters( - newSyntax = jsonTypeSwitch[Message, (TypeA, TypeB)], - oldSyntax = jsonTypeSwitch[Message, (TypeA, TypeB)] - ) { - val m: Seq[Message] = List( - TypeA.ClassA1(23), - TypeA.ClassA2("world"), - TypeB.ClassB1(valid = false), - TypeB.ClassB2(Seq("a23", "c62"))) - - val jsons = m.map(JSON[Message].write) - jsons must be( - List( - JObject("number" -> JLong(23), "type" -> JString("ClassA1")), - JObject("name" -> JString("world"), "type" -> JString("ClassA2")), - JObject("valid" -> JBool(false), "type" -> JString("ClassB1")), - JObject( - "references" -> JArray(List(JString("a23"), JString("c62"))), - "type" -> JString("ClassB2")) - )) - - val messages = jsons.map(JSON[Message].read).map(_.toOption.get) - messages must be(m) - } - - { - given JSON[B] = new JSON[B] { - override def read(jval: JValue): JValidation[B] = jval match { - case JObject(List(_, "field" -> JString(s"Custom-B-${n}"))) => - Valid(B(n.toInt)) - case _ => ??? - } - - override def write(value: B): JValue = - JObject(List("field" -> JString(s"Custom-B-${value.int}"))) - } - - "handle custom implementations for subtypes".withFormatters( - newSyntax = jsonTypeSwitch[A, (B, D, C)], - oldSyntax = jsonTypeSwitch[A, B, D, C](Nil) - ) { - check[A](D(2345), """ {"type": "D2", "int": 2345 } """) - check[A](C(4), """ {"type": "C", "int": 4 } """) - check[A](B(34), """ {"type": "B", "field": "Custom-B-34" } """) - } - } +// { +// given JSON[B] = deriveJSON[B] +// "derive a subset of a sealed trait".withFormatters( +// newSyntax = jsonTypeSwitch[A, (B, C)], +// oldSyntax = jsonTypeSwitch[A, B, C](Nil) +// ) { +// val b = B(123) +// val jsonB = JSON[A].write(b) +// +// val b2 = JSON[A].read(jsonB).getOrElse(null) +// +// b2 must be(b) +// +// val c = C(2345345) +// val jsonC = JSON[A].write(c) +// +// val c2 = JSON[A].read(jsonC).getOrElse(null) +// +// c2 must be(c) +// } +// } + +// "derive a subset of a sealed trait with a mongoKey".withFormatters( +// newSyntax = jsonTypeSwitch[A, (B, D)], +// oldSyntax = jsonTypeSwitch[A, B, D](Nil) +// ) { +// val d = D(123) +// val json = JSON[A].write(d) +// val d2 = JSON[A].read(json) +// +// (json \ "type").as[String] must be("D2") +// d2 must be(Valid(d)) +// } + +// "combine different sum types tree".withFormatters( +// newSyntax = jsonTypeSwitch[Message, (TypeA, TypeB)], +// oldSyntax = jsonTypeSwitch[Message, TypeA, TypeB](Nil) +// ) { +// val m: Seq[Message] = List( +// TypeA.ClassA1(23), +// TypeA.ClassA2("world"), +// TypeB.ClassB1(valid = false), +// TypeB.ClassB2(Seq("a23", "c62"))) +// +// val jsons = m.map(JSON[Message].write) +// jsons must be( +// List( +// JObject("number" -> JLong(23), "type" -> JString("ClassA1")), +// JObject("name" -> JString("world"), "type" -> JString("ClassA2")), +// JObject("valid" -> JBool(false), "type" -> JString("ClassB1")), +// JObject( +// "references" -> JArray(List(JString("a23"), JString("c62"))), +// "type" -> JString("ClassB2")) +// )) +// +// val messages = jsons.map(JSON[Message].read).map(_.toOption.get) +// messages must be(m) +// } + +// { +// given JSON[B] = new JSON[B] { +// override def read(jval: JValue): JValidation[B] = jval match { +// case JObject(List(_, "field" -> JString(s"Custom-B-${n}"))) => +// Valid(B(n.toInt)) +// case _ => ??? +// } +// +// override def write(value: B): JValue = +// JObject(List("field" -> JString(s"Custom-B-${value.int}"))) +// } +// +// "handle custom implementations for subtypes".withFormatters( +// newSyntax = jsonTypeSwitch[A, (B, D, C)], +// oldSyntax = jsonTypeSwitch[A, B, D, C](Nil) +// ) { +// check[A](D(2345), """ {"type": "D2", "int": 2345 } """) +// check[A](C(4), """ {"type": "C", "int": 4 } """) +// check[A](B(34), """ {"type": "B", "field": "Custom-B-34" } """) +// } +// } "handle PlatformFormattedNotification case" in { From 54cb9657a0768ade2164932c3878b14ee7e9f3c0 Mon Sep 17 00:00:00 2001 From: benkobalog Date: Mon, 30 Jun 2025 10:44:53 +0200 Subject: [PATCH 130/142] Api backwards compatibility fix --- .../main/scala-3/io/sphere/json/ToJSON.scala | 2 +- .../io/sphere/json/generic/DeriveToJSON.scala | 2 +- .../sphere/json/generic/JSONTypeSwitch.scala | 41 +++++++++++-------- .../scala/io/sphere/json/ToJSONSpec.scala | 2 +- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/json/json-core/src/main/scala-3/io/sphere/json/ToJSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/ToJSON.scala index 96870ed9..57ececc1 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/ToJSON.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/ToJSON.scala @@ -19,7 +19,7 @@ object ToJSON extends ToJSONInstances with ToJSONCatsInstances with generic.Deri /** construct an instance from a function */ - def instance[T](toFs: Formatters[ToJSON])(toJson: T => JValue): ToJSON[T] = new ToJSON[T] { + def instance[T](toJson: T => JValue, toFs: Formatters[ToJSON] = null): ToJSON[T] = new ToJSON[T] { override def write(value: T): JValue = toJson(value) override val toFormatters: Formatters[ToJSON] = toFs } diff --git a/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveToJSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveToJSON.scala index 581c3fcb..703e91e1 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveToJSON.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveToJSON.scala @@ -24,7 +24,7 @@ trait DeriveToJSON { val caseClassMetaData: TypeMetaData = AnnotationReader.readTypeMetaData[A] val toJsons: Vector[ToJSON[Any]] = summonToJson[mirrorOfProduct.MirroredElemTypes] - ToJSON.instance(null) { value => + ToJSON.instance { value => val caseClassFields = value.asInstanceOf[Product].productIterator toJsons.iterator .zip(caseClassFields) diff --git a/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala index 48bbe1a0..37765dc3 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala @@ -10,22 +10,28 @@ import scala.compiletime.{constValue, constValueTuple} object JSONTypeSwitch { import scala.compiletime.{erasedValue, summonInline} - case class Formatters[JsonF[_]]( + case class Formatters[JsonKind[_]]( serializedTypeNames: Map[String, String], - forCaseClasses: Map[String, JsonF[Any]], + forCaseClasses: Map[String, JsonKind[Any]], typeDiscriminator: String ) { - def addTypeNames(names: Map[String, String]): Formatters[JsonF] = + def addTypeNames(names: Map[String, String]): Formatters[JsonKind] = copy(serializedTypeNames = serializedTypeNames ++ names) } object Formatters { - def merge[JsonF[_]](f1: Formatters[JsonF], f2: Formatters[JsonF]): Formatters[JsonF] = - Formatters[JsonF]( + def merge[JsonKind[_]]( + f1: Formatters[JsonKind], + f2: Formatters[JsonKind]): Formatters[JsonKind] = { + require( + f1.typeDiscriminator == f2.typeDiscriminator, + "Only a single @JSONTypeHintField is allowed") + Formatters[JsonKind]( serializedTypeNames = f1.serializedTypeNames ++ f2.serializedTypeNames, forCaseClasses = f1.forCaseClasses ++ f2.forCaseClasses, typeDiscriminator = f1.typeDiscriminator ) + } } inline def deriveToFormatters[SuperType, SubTypes <: Tuple]: Formatters[ToJSON] = { @@ -45,17 +51,20 @@ object JSONTypeSwitch { } inline def toJsonTypeSwitch[SuperType](formatters: Formatters[ToJSON]): ToJSON[SuperType] = - ToJSON.instance(formatters) { a => - val scalaTypeName = a.asInstanceOf[Product].productPrefix - val serializedTypeName = formatters.serializedTypeNames(scalaTypeName) - val jsonObj = formatters.forCaseClasses(scalaTypeName).write(a) match { - case JObject(obj) => obj - case json => - throw new Exception(s"This code only handles objects as of now, but got: $json") - } - val typeDiscriminator = formatters.typeDiscriminator -> JString(serializedTypeName) - JObject(typeDiscriminator :: jsonObj) - } + ToJSON.instance( + toJson = { scalaValue => + val scalaTypeName = scalaValue.asInstanceOf[Product].productPrefix + val serializedTypeName = formatters.serializedTypeNames(scalaTypeName) + val jsonObj = formatters.forCaseClasses(scalaTypeName).write(scalaValue) match { + case JObject(obj) => obj + case json => + throw new Exception(s"This code only handles objects as of now, but got: $json") + } + val typeDiscriminator = formatters.typeDiscriminator -> JString(serializedTypeName) + JObject(typeDiscriminator :: jsonObj) + }, + toFs = formatters + ) inline def fromJsonTypeSwitch[SuperType]( formatters: Formatters[FromJSON]): FromJSON[SuperType] = { diff --git a/json/json-core/src/test/scala/io/sphere/json/ToJSONSpec.scala b/json/json-core/src/test/scala/io/sphere/json/ToJSONSpec.scala index 6215e228..acaeea18 100644 --- a/json/json-core/src/test/scala/io/sphere/json/ToJSONSpec.scala +++ b/json/json-core/src/test/scala/io/sphere/json/ToJSONSpec.scala @@ -12,7 +12,7 @@ class ToJSONSpec extends AnyWordSpec with Matchers { "ToJSON.apply" must { "create a ToJSON" in { - implicit val encodeUser: ToJSON[User] = ToJSON.instance[User](null)(u => + implicit val encodeUser: ToJSON[User] = ToJSON.instance[User](u => JObject( List( "id" -> toJValue(u.id), From ef94c444ddf53f3f1a9abe8b8d5cd754a70aacff Mon Sep 17 00:00:00 2001 From: benkobalog Date: Mon, 30 Jun 2025 10:45:32 +0200 Subject: [PATCH 131/142] scalafmt --- .../main/scala-3/io/sphere/json/generic/DeriveFromJSON.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveFromJSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveFromJSON.scala index f2a8d4fc..994aafc0 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveFromJSON.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveFromJSON.scala @@ -50,7 +50,7 @@ trait DeriveFromJSON { Validated.invalidNel(JSONParseError(s"JSON object expected. $x")) }, fieldSet = fieldNames.toSet, - fromFs = null, + fromFs = null ) } From e33816a519fcd2e9024fe891b550ceaf187a2574 Mon Sep 17 00:00:00 2001 From: benkobalog Date: Mon, 30 Jun 2025 10:50:44 +0200 Subject: [PATCH 132/142] minor refactor --- .../sphere/json/generic/DeriveSingleton.scala | 2 +- .../sphere/json/generic/JSONTypeSwitch.scala | 56 +++++++++---------- .../io/sphere/mongo/generic/generic.scala | 2 +- util/src/main/scala-3/AnnotationReader.scala | 2 +- 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveSingleton.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveSingleton.scala index 68c2c507..9f932e54 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveSingleton.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/generic/DeriveSingleton.scala @@ -37,7 +37,7 @@ object DeriveSingleton { inline private def deriveTrait[A](mirrorOfSum: Mirror.SumOf[A]): DeriveSingleton[A] = { val traitMetaData: TraitMetaData = AnnotationReader.readTraitMetaData[A] - val typeHintMap = traitMetaData.subTypeSerializedTypeNames + val typeHintMap = traitMetaData.serializedNamesOfSubTypes val reverseTypeHintMap: Map[String, String] = typeHintMap.map((on, n) => (n, on)) val jsons: Seq[DeriveSingleton[Any]] = summonFormatters[mirrorOfSum.MirroredElemTypes] diff --git a/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala index 37765dc3..322627d1 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala @@ -10,36 +10,12 @@ import scala.compiletime.{constValue, constValueTuple} object JSONTypeSwitch { import scala.compiletime.{erasedValue, summonInline} - case class Formatters[JsonKind[_]]( - serializedTypeNames: Map[String, String], - forCaseClasses: Map[String, JsonKind[Any]], - typeDiscriminator: String - ) { - def addTypeNames(names: Map[String, String]): Formatters[JsonKind] = - copy(serializedTypeNames = serializedTypeNames ++ names) - } - - object Formatters { - def merge[JsonKind[_]]( - f1: Formatters[JsonKind], - f2: Formatters[JsonKind]): Formatters[JsonKind] = { - require( - f1.typeDiscriminator == f2.typeDiscriminator, - "Only a single @JSONTypeHintField is allowed") - Formatters[JsonKind]( - serializedTypeNames = f1.serializedTypeNames ++ f2.serializedTypeNames, - forCaseClasses = f1.forCaseClasses ++ f2.forCaseClasses, - typeDiscriminator = f1.typeDiscriminator - ) - } - } - inline def deriveToFormatters[SuperType, SubTypes <: Tuple]: Formatters[ToJSON] = { val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] summonToFormatters[SubTypes]() .reduce(Formatters.merge) .copy(typeDiscriminator = traitMetaData.typeDiscriminator) - .addTypeNames(traitMetaData.subTypeSerializedTypeNames) + .addTypeNames(traitMetaData.serializedNamesOfSubTypes) } inline def deriveFromFormatters[SuperType, SubTypes <: Tuple]: Formatters[FromJSON] = { @@ -47,7 +23,7 @@ object JSONTypeSwitch { summonFromFormatters[SubTypes]() .reduce(Formatters.merge) .copy(typeDiscriminator = traitMetaData.typeDiscriminator) - .addTypeNames(traitMetaData.subTypeSerializedTypeNames) + .addTypeNames(traitMetaData.serializedNamesOfSubTypes) } inline def toJsonTypeSwitch[SuperType](formatters: Formatters[ToJSON]): ToJSON[SuperType] = @@ -113,7 +89,7 @@ object JSONTypeSwitch { Map(traitMetaData.top.scalaName -> headFormatter) val f = Formatters[FromJSON]( - serializedTypeNames = traitMetaData.subTypeSerializedTypeNames, + serializedTypeNames = traitMetaData.serializedNamesOfSubTypes, forCaseClasses = formatterMap, typeDiscriminator = traitMetaData.typeDiscriminator ) @@ -134,11 +110,35 @@ object JSONTypeSwitch { Map(traitMetaData.top.scalaName -> headFormatter) val f = Formatters[ToJSON]( - serializedTypeNames = traitMetaData.subTypeSerializedTypeNames, + serializedTypeNames = traitMetaData.serializedNamesOfSubTypes, forCaseClasses = formatterMap, typeDiscriminator = traitMetaData.typeDiscriminator ) summonToFormatters[ts](acc :+ f) } + case class Formatters[JsonKind[_]]( + serializedTypeNames: Map[String, String], + forCaseClasses: Map[String, JsonKind[Any]], + typeDiscriminator: String + ) { + def addTypeNames(names: Map[String, String]): Formatters[JsonKind] = + copy(serializedTypeNames = serializedTypeNames ++ names) + } + + object Formatters { + def merge[JsonKind[_]]( + f1: Formatters[JsonKind], + f2: Formatters[JsonKind]): Formatters[JsonKind] = { + require( + f1.typeDiscriminator == f2.typeDiscriminator, + "@JSONTypeHintField has to be the same on all traits") + Formatters[JsonKind]( + serializedTypeNames = f1.serializedTypeNames ++ f2.serializedTypeNames, + forCaseClasses = f1.forCaseClasses ++ f2.forCaseClasses, + typeDiscriminator = f1.typeDiscriminator + ) + } + } + } diff --git a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala index 0c1c9b38..c32d752e 100644 --- a/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala +++ b/mongo/mongo-core/src/main/scala-3/io/sphere/mongo/generic/generic.scala @@ -18,7 +18,7 @@ def mongoEnum(e: Enumeration): MongoFormat[e.Value] = new MongoFormat[e.Value] { inline def mongoTypeSwitch[SuperType, SubTypeTuple <: Tuple]: MongoFormat[SuperType] = { val traitMetaData = MongoAnnotationReader.readTraitMetaData[SuperType] - val typeHintMap = traitMetaData.subTypeSerializedTypeNames + val typeHintMap = traitMetaData.serializedNamesOfSubTypes val reverseTypeHintMap = typeHintMap.map((on, n) => (n, on)) val formatters = summonFormatters[SubTypeTuple]() val subTypeNames = summonMetaData[SubTypeTuple]() diff --git a/util/src/main/scala-3/AnnotationReader.scala b/util/src/main/scala-3/AnnotationReader.scala index 30f5dfff..fc56d6b9 100644 --- a/util/src/main/scala-3/AnnotationReader.scala +++ b/util/src/main/scala-3/AnnotationReader.scala @@ -34,7 +34,7 @@ case class TraitMetaData( val typeDiscriminator: String = typeHintFieldRaw.getOrElse(defaultTypeDiscriminatorName) - val subTypeSerializedTypeNames: Map[String, String] = subtypes.map { + val serializedNamesOfSubTypes: Map[String, String] = subtypes.map { case (scalaName, classMeta) => scalaName -> classMeta.typeHint.getOrElse(scalaName) } } From 5583c764f805dc36a3e7dfc2c9dfe1e13df11c48 Mon Sep 17 00:00:00 2001 From: benkobalog Date: Mon, 30 Jun 2025 11:01:25 +0200 Subject: [PATCH 133/142] scalafmt --- util/src/main/scala-3/AnnotationReader.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/util/src/main/scala-3/AnnotationReader.scala b/util/src/main/scala-3/AnnotationReader.scala index fc56d6b9..2b53a4fd 100644 --- a/util/src/main/scala-3/AnnotationReader.scala +++ b/util/src/main/scala-3/AnnotationReader.scala @@ -34,8 +34,8 @@ case class TraitMetaData( val typeDiscriminator: String = typeHintFieldRaw.getOrElse(defaultTypeDiscriminatorName) - val serializedNamesOfSubTypes: Map[String, String] = subtypes.map { - case (scalaName, classMeta) => scalaName -> classMeta.typeHint.getOrElse(scalaName) + val serializedNamesOfSubTypes: Map[String, String] = subtypes.map { case (scalaName, classMeta) => + scalaName -> classMeta.typeHint.getOrElse(scalaName) } } From 415cc9db8a39162f359c49c96ea9275a73aefdc7 Mon Sep 17 00:00:00 2001 From: benkobalog Date: Thu, 24 Jul 2025 12:19:53 +0200 Subject: [PATCH 134/142] reenable tests in JsonTypeSwitchSpec --- .../json/generic/JsonTypeSwitchSpec.scala | 160 +++++++++--------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala index f4f2f34f..95a7539e 100644 --- a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala +++ b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala @@ -15,86 +15,86 @@ class JsonTypeSwitchSpec extends AnyWordSpec with Matchers { "jsonTypeSwitch" must { -// { -// given JSON[B] = deriveJSON[B] -// "derive a subset of a sealed trait".withFormatters( -// newSyntax = jsonTypeSwitch[A, (B, C)], -// oldSyntax = jsonTypeSwitch[A, B, C](Nil) -// ) { -// val b = B(123) -// val jsonB = JSON[A].write(b) -// -// val b2 = JSON[A].read(jsonB).getOrElse(null) -// -// b2 must be(b) -// -// val c = C(2345345) -// val jsonC = JSON[A].write(c) -// -// val c2 = JSON[A].read(jsonC).getOrElse(null) -// -// c2 must be(c) -// } -// } - -// "derive a subset of a sealed trait with a mongoKey".withFormatters( -// newSyntax = jsonTypeSwitch[A, (B, D)], -// oldSyntax = jsonTypeSwitch[A, B, D](Nil) -// ) { -// val d = D(123) -// val json = JSON[A].write(d) -// val d2 = JSON[A].read(json) -// -// (json \ "type").as[String] must be("D2") -// d2 must be(Valid(d)) -// } - -// "combine different sum types tree".withFormatters( -// newSyntax = jsonTypeSwitch[Message, (TypeA, TypeB)], -// oldSyntax = jsonTypeSwitch[Message, TypeA, TypeB](Nil) -// ) { -// val m: Seq[Message] = List( -// TypeA.ClassA1(23), -// TypeA.ClassA2("world"), -// TypeB.ClassB1(valid = false), -// TypeB.ClassB2(Seq("a23", "c62"))) -// -// val jsons = m.map(JSON[Message].write) -// jsons must be( -// List( -// JObject("number" -> JLong(23), "type" -> JString("ClassA1")), -// JObject("name" -> JString("world"), "type" -> JString("ClassA2")), -// JObject("valid" -> JBool(false), "type" -> JString("ClassB1")), -// JObject( -// "references" -> JArray(List(JString("a23"), JString("c62"))), -// "type" -> JString("ClassB2")) -// )) -// -// val messages = jsons.map(JSON[Message].read).map(_.toOption.get) -// messages must be(m) -// } - -// { -// given JSON[B] = new JSON[B] { -// override def read(jval: JValue): JValidation[B] = jval match { -// case JObject(List(_, "field" -> JString(s"Custom-B-${n}"))) => -// Valid(B(n.toInt)) -// case _ => ??? -// } -// -// override def write(value: B): JValue = -// JObject(List("field" -> JString(s"Custom-B-${value.int}"))) -// } -// -// "handle custom implementations for subtypes".withFormatters( -// newSyntax = jsonTypeSwitch[A, (B, D, C)], -// oldSyntax = jsonTypeSwitch[A, B, D, C](Nil) -// ) { -// check[A](D(2345), """ {"type": "D2", "int": 2345 } """) -// check[A](C(4), """ {"type": "C", "int": 4 } """) -// check[A](B(34), """ {"type": "B", "field": "Custom-B-34" } """) -// } -// } + { + given JSON[B] = deriveJSON[B] + "derive a subset of a sealed trait".withFormatters( + newSyntax = jsonTypeSwitch[A, (B, C)], + oldSyntax = jsonTypeSwitch[A, B, C](Nil) + ) { + val b = B(123) + val jsonB = JSON[A].write(b) + + val b2 = JSON[A].read(jsonB).getOrElse(null) + + b2 must be(b) + + val c = C(2345345) + val jsonC = JSON[A].write(c) + + val c2 = JSON[A].read(jsonC).getOrElse(null) + + c2 must be(c) + } + } + + "derive a subset of a sealed trait with a mongoKey".withFormatters( + newSyntax = jsonTypeSwitch[A, (B, D)], + oldSyntax = jsonTypeSwitch[A, B, D](Nil) + ) { + val d = D(123) + val json = JSON[A].write(d) + val d2 = JSON[A].read(json) + + (json \ "type").as[String] must be("D2") + d2 must be(Valid(d)) + } + + "combine different sum types tree".withFormatters( + newSyntax = jsonTypeSwitch[Message, (TypeA, TypeB)], + oldSyntax = jsonTypeSwitch[Message, TypeA, TypeB](Nil) + ) { + val m: Seq[Message] = List( + TypeA.ClassA1(23), + TypeA.ClassA2("world"), + TypeB.ClassB1(valid = false), + TypeB.ClassB2(Seq("a23", "c62"))) + + val jsons = m.map(JSON[Message].write) + jsons must be( + List( + JObject("number" -> JLong(23), "type" -> JString("ClassA1")), + JObject("name" -> JString("world"), "type" -> JString("ClassA2")), + JObject("valid" -> JBool(false), "type" -> JString("ClassB1")), + JObject( + "references" -> JArray(List(JString("a23"), JString("c62"))), + "type" -> JString("ClassB2")) + )) + + val messages = jsons.map(JSON[Message].read).map(_.toOption.get) + messages must be(m) + } + + { + given JSON[B] = new JSON[B] { + override def read(jval: JValue): JValidation[B] = jval match { + case JObject(List(_, "field" -> JString(s"Custom-B-${n}"))) => + Valid(B(n.toInt)) + case _ => ??? + } + + override def write(value: B): JValue = + JObject(List("field" -> JString(s"Custom-B-${value.int}"))) + } + + "handle custom implementations for subtypes".withFormatters( + newSyntax = jsonTypeSwitch[A, (B, D, C)], + oldSyntax = jsonTypeSwitch[A, B, D, C](Nil) + ) { + check[A](D(2345), """ {"type": "D2", "int": 2345 } """) + check[A](C(4), """ {"type": "C", "int": 4 } """) + check[A](B(34), """ {"type": "B", "field": "Custom-B-34" } """) + } + } "handle PlatformFormattedNotification case" in { From 84c39741b4f8334b6c5952016f7fb135bbc25a07 Mon Sep 17 00:00:00 2001 From: benkobalog Date: Thu, 24 Jul 2025 13:02:29 +0200 Subject: [PATCH 135/142] Move scala2 specific test to their folder --- .../io/sphere/json/DeriveJSONCompatibilitySpec.scala | 5 ++--- .../io/sphere/json/generic/SubTypeNameSpec.scala | 0 2 files changed, 2 insertions(+), 3 deletions(-) rename json/json-derivation/src/test/{scala => scala-2}/io/sphere/json/DeriveJSONCompatibilitySpec.scala (95%) rename json/json-derivation/src/test/{scala => scala-2}/io/sphere/json/generic/SubTypeNameSpec.scala (100%) diff --git a/json/json-derivation/src/test/scala/io/sphere/json/DeriveJSONCompatibilitySpec.scala b/json/json-derivation/src/test/scala-2/io/sphere/json/DeriveJSONCompatibilitySpec.scala similarity index 95% rename from json/json-derivation/src/test/scala/io/sphere/json/DeriveJSONCompatibilitySpec.scala rename to json/json-derivation/src/test/scala-2/io/sphere/json/DeriveJSONCompatibilitySpec.scala index 9284d7b0..38da7203 100644 --- a/json/json-derivation/src/test/scala/io/sphere/json/DeriveJSONCompatibilitySpec.scala +++ b/json/json-derivation/src/test/scala-2/io/sphere/json/DeriveJSONCompatibilitySpec.scala @@ -1,13 +1,12 @@ package io.sphere.json -import java.util.UUID +import io.sphere.json.generic.{deriveJSON, jsonProduct} import org.scalacheck.{Arbitrary, Gen} import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks -import io.sphere.json.generic.deriveJSON -import io.sphere.json.generic.jsonProduct +import java.util.UUID class DeriveJSONCompatibilitySpec extends AnyWordSpec with Matchers with ScalaCheckPropertyChecks { diff --git a/json/json-derivation/src/test/scala/io/sphere/json/generic/SubTypeNameSpec.scala b/json/json-derivation/src/test/scala-2/io/sphere/json/generic/SubTypeNameSpec.scala similarity index 100% rename from json/json-derivation/src/test/scala/io/sphere/json/generic/SubTypeNameSpec.scala rename to json/json-derivation/src/test/scala-2/io/sphere/json/generic/SubTypeNameSpec.scala From 687c7528353fd717e0b41eb2df4dfba1543dd01d Mon Sep 17 00:00:00 2001 From: benkobalog Date: Thu, 24 Jul 2025 14:54:48 +0200 Subject: [PATCH 136/142] Add tests for subTypeNames in JSON --- build.sbt | 2 +- .../main/scala-3/io/sphere/json/JSON.scala | 11 +--- .../sphere/json/generic/JSONTypeSwitch.scala | 4 +- .../io/sphere/json/generic/generic.scala | 6 ++ .../sphere/json/generic/SubTypeNameSpec.scala | 22 +++++++ .../json/generic/JsonTypeSwitchSpec.scala | 3 +- .../sphere/json/generic/SubTypeNameSpec.scala | 63 +++++++++++++++++++ 7 files changed, 100 insertions(+), 11 deletions(-) create mode 100644 json/json-derivation/src/test/scala-3/io/sphere/json/generic/SubTypeNameSpec.scala diff --git a/build.sbt b/build.sbt index 1d6d9128..41f85f98 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ lazy val scala3 = "3.3.5" // sbt-github-actions needs configuration in `ThisBuild` ThisBuild / crossScalaVersions := Seq(scala213, scala3) -ThisBuild / scalaVersion := scala213 +ThisBuild / scalaVersion := scala3 ThisBuild / githubWorkflowPublishTargetBranches := List() ThisBuild / githubWorkflowJavaVersions := List(JavaSpec.temurin("21")) ThisBuild / githubWorkflowBuildPreamble ++= List( diff --git a/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala index 087c2363..f0d4b06b 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala @@ -1,21 +1,15 @@ package io.sphere.json import cats.implicits.* -import io.sphere.json.generic.JSONTypeSwitch.Formatters +import io.sphere.json.generic.JSONTypeSwitch.{Formatters, fromJsonTypeSwitch} import org.json4s.JsonAST.JValue -import scala.deriving.Mirror - trait JSON[A] extends FromJSON[A] with ToJSON[A] { // This field is only used in case we derive a trait, for classes/objects it remains empty // It uses the JSON names not the Scala names (if there's @JSONTypeHint renaming a class the renamed name is used here) def subTypeNames: List[String] = Nil } -inline def deriveJSON[A](using Mirror.Of[A]): JSON[A] = JSON.derived -inline def deriveToJSON[A](using Mirror.Of[A]): ToJSON[A] = ToJSON.derived -inline def deriveFromJSON[A](using Mirror.Of[A]): FromJSON[A] = FromJSON.derived - object JSON extends JSONCatsInstances { inline def apply[A: JSON]: JSON[A] = summon[JSON[A]] inline given derived[A](using fromJSON: FromJSON[A], toJSON: ToJSON[A]): JSON[A] = @@ -24,7 +18,8 @@ object JSON extends JSONCatsInstances { writeFn = toJSON.write, fromFs = fromJSON.fromFormatters, toFs = toJSON.toFormatters, - fieldSet = fromJSON.fields + fieldSet = fromJSON.fields, + subTypeNameList = Option(fromJSON.fromFormatters).map(_.getSubTypeNames).getOrElse(Nil) ) def instance[A]( diff --git a/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala index 322627d1..e8a5a4a4 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala @@ -68,7 +68,7 @@ object JSONTypeSwitch { JSON.instance( writeFn = toJson.write, readFn = fromJson.read, - subTypeNameList = fromFormatters.serializedTypeNames.values.toList, + subTypeNameList = fromFormatters.getSubTypeNames, fromFs = fromJson.fromFormatters, toFs = toJson.toFormatters ) @@ -124,6 +124,8 @@ object JSONTypeSwitch { ) { def addTypeNames(names: Map[String, String]): Formatters[JsonKind] = copy(serializedTypeNames = serializedTypeNames ++ names) + + def getSubTypeNames: List[String] = serializedTypeNames.values.toList } object Formatters { diff --git a/json/json-core/src/main/scala-3/io/sphere/json/generic/generic.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/generic.scala index df774e76..547c129d 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/generic/generic.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/generic/generic.scala @@ -2,6 +2,12 @@ package io.sphere.json.generic import io.sphere.json.* +import scala.deriving.Mirror + +inline def deriveJSON[A](using Mirror.Of[A]): JSON[A] = JSON.derived +inline def deriveToJSON[A](using Mirror.Of[A]): ToJSON[A] = ToJSON.derived +inline def deriveFromJSON[A](using Mirror.Of[A]): FromJSON[A] = FromJSON.derived + /** Creates a ToJSON instance for an Enumeration type that encodes the `toString` representations of * the enumeration values. */ diff --git a/json/json-derivation/src/test/scala-2/io/sphere/json/generic/SubTypeNameSpec.scala b/json/json-derivation/src/test/scala-2/io/sphere/json/generic/SubTypeNameSpec.scala index 2a2912ac..b758e035 100644 --- a/json/json-derivation/src/test/scala-2/io/sphere/json/generic/SubTypeNameSpec.scala +++ b/json/json-derivation/src/test/scala-2/io/sphere/json/generic/SubTypeNameSpec.scala @@ -33,6 +33,16 @@ class SubTypeNameSpec extends AnyWordSpec with Matchers { format.asInstanceOf[TypeSelectorContainer].typeSelectors.map(_.typeValue) must be( subTypeNames) } + + "return only class names in nested trait hierarchies" in { + val format: JSON[SuperType2] = + jsonTypeSwitch[SuperType2, SubType1, SubType2](Nil) + + val names = + List("SubClass1A", "SubClass1A", "SubType1", "SubClass2A", "SubClass2A", "SubType2") + format.asInstanceOf[TypeSelectorContainer].typeSelectors.map(_.typeValue) must be(names) + format.subTypeNames must be(names) + } } } @@ -42,4 +52,16 @@ object SubTypeNameSpec { @JSONTypeHint("Obj2") case object ObjHidden extends SuperType case class Class1(int: Int) extends SuperType @JSONTypeHint("Class2") case class ClassHidden(int: Int) extends SuperType + + sealed trait SuperType2 + sealed trait SubType1 extends SuperType2 + object SubType1 { + case class SubClass1A(x: Int) extends SubType1 + implicit val json: JSON[SubType1] = deriveJSON + } + sealed trait SubType2 extends SuperType2 + object SubType2 { + case class SubClass2A(x: Int) extends SubType2 + implicit val json: JSON[SubType2] = deriveJSON + } } diff --git a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala index 95a7539e..63f496cf 100644 --- a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala +++ b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala @@ -2,7 +2,8 @@ package io.sphere.json.generic import cats.data.Validated.Valid import cats.implicits.toTraverseOps -import io.sphere.json.{JSON, JSONParseError, JValidation, deriveJSON, parseJSON} +import io.sphere.json.{JSON, JSONParseError, JValidation, parseJSON} +import io.sphere.json.generic.deriveJSON import io.sphere.json.generic.jsonTypeSwitch import org.json4s.JsonAST.JObject import org.scalatest.matchers.must.Matchers diff --git a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/SubTypeNameSpec.scala b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/SubTypeNameSpec.scala new file mode 100644 index 00000000..28165f04 --- /dev/null +++ b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/SubTypeNameSpec.scala @@ -0,0 +1,63 @@ +package io.sphere.json.generic + +import io.sphere.json.JSON +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec +import io.sphere.json.generic.deriveJSON + +class SubTypeNameSpec extends AnyWordSpec with Matchers { + import SubTypeNameSpec._ + + "JSON.subtypeNames" must { + + val subTypeNames = List("Obj1", "Obj2", "Class1", "Class2") + "return all subtypes of a trait when using deriveJSON" in { + val format: JSON[SuperType] = deriveJSON + + format.subTypeNames must be(subTypeNames) + + } + + "return all subtypes of a trait when using jsonTypeSwitch" in { + implicit val obj1F: JSON[Obj1.type] = deriveJSON + implicit val objHF: JSON[ObjHidden.type] = deriveJSON + implicit val class1F: JSON[Class1] = deriveJSON + implicit val classhF: JSON[ClassHidden] = deriveJSON + + val format: JSON[SuperType] = + jsonTypeSwitch[SuperType, (Obj1.type, ObjHidden.type, Class1, ClassHidden)] + + format.subTypeNames must be(subTypeNames) + + } + + "return only class names in nested trait hierarchies" in { + val format: JSON[SuperType2] = + jsonTypeSwitch[SuperType2, (SubType1, SubType2)] + + val names = + List("SubClass1A", "SubClass2A", "SubType1", "SubType2") + format.subTypeNames must be(names) + } + } +} + +object SubTypeNameSpec { + sealed trait SuperType + case object Obj1 extends SuperType + @JSONTypeHint("Obj2") case object ObjHidden extends SuperType + case class Class1(int: Int) extends SuperType + @JSONTypeHint("Class2") case class ClassHidden(int: Int) extends SuperType + + sealed trait SuperType2 + sealed trait SubType1 extends SuperType2 + object SubType1 { + case class SubClass1A(x: Int) extends SubType1 + given JSON[SubType1] = deriveJSON + } + sealed trait SubType2 extends SuperType2 + object SubType2 { + case class SubClass2A(x: Int) extends SubType2 + given JSON[SubType2] = deriveJSON + } +} From e6254cc5eacea0b5040e8b09baaf962599c6cff5 Mon Sep 17 00:00:00 2001 From: benkobalog Date: Thu, 24 Jul 2025 16:21:38 +0200 Subject: [PATCH 137/142] set sbt ThisBuild/scalaversion back to 2.13 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 41f85f98..1d6d9128 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ lazy val scala3 = "3.3.5" // sbt-github-actions needs configuration in `ThisBuild` ThisBuild / crossScalaVersions := Seq(scala213, scala3) -ThisBuild / scalaVersion := scala3 +ThisBuild / scalaVersion := scala213 ThisBuild / githubWorkflowPublishTargetBranches := List() ThisBuild / githubWorkflowJavaVersions := List(JavaSpec.temurin("21")) ThisBuild / githubWorkflowBuildPreamble ++= List( From 2cea05258f5baf7a13b9bf1d6f14d83009ee815e Mon Sep 17 00:00:00 2001 From: benkobalog Date: Fri, 25 Jul 2025 12:23:56 +0200 Subject: [PATCH 138/142] Use Class instead of String for ToFormatters --- .../scala-3/io/sphere/json/FromJSON.scala | 8 +- .../main/scala-3/io/sphere/json/JSON.scala | 34 +++-- .../main/scala-3/io/sphere/json/ToJSON.scala | 9 +- .../sphere/json/generic/JSONTypeSwitch.scala | 130 +++++++++++------- .../json/generic/JsonTypeSwitchSpec.scala | 58 ++++++-- .../sphere/json/generic/SubTypeNameSpec.scala | 4 +- util/src/main/scala-3/AnnotationReader.scala | 2 + 7 files changed, 157 insertions(+), 88 deletions(-) diff --git a/json/json-core/src/main/scala-3/io/sphere/json/FromJSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/FromJSON.scala index f8ddbad3..e1111c56 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/FromJSON.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/FromJSON.scala @@ -1,7 +1,7 @@ package io.sphere.json import io.sphere.json.JValidation -import io.sphere.json.generic.JSONTypeSwitch.Formatters +import io.sphere.json.generic.JSONTypeSwitch.FromFormatters import org.json4s.JsonAST.JValue /** Type class for types that can be read from JSON. */ @@ -13,7 +13,7 @@ trait FromJSON[A] extends Serializable { val fields: Set[String] = FromJSON.emptyFieldsSet // This is automatically filled for traits - val fromFormatters: Formatters[FromJSON] = null + val fromFormatters: FromFormatters = null } object FromJSON extends FromJSONInstances with FromJSONCatsInstances with generic.DeriveFromJSON { @@ -23,11 +23,11 @@ object FromJSON extends FromJSONInstances with FromJSONCatsInstances with generi def instance[A]( readFn: JValue => JValidation[A], - fromFs: Formatters[FromJSON], + fromFs: FromFormatters, fieldSet: Set[String] = emptyFieldsSet): FromJSON[A] = new { override def read(jval: JValue): JValidation[A] = readFn(jval) override val fields: Set[String] = fieldSet - override val fromFormatters: Formatters[FromJSON] = fromFs + override val fromFormatters: FromFormatters = fromFs } } diff --git a/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala index f0d4b06b..6aad621e 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala @@ -1,7 +1,7 @@ package io.sphere.json import cats.implicits.* -import io.sphere.json.generic.JSONTypeSwitch.{Formatters, fromJsonTypeSwitch} +import io.sphere.json.generic.JSONTypeSwitch.{FromFormatters, ToFormatters, fromJsonTypeSwitch} import org.json4s.JsonAST.JValue trait JSON[A] extends FromJSON[A] with ToJSON[A] { @@ -19,23 +19,33 @@ object JSON extends JSONCatsInstances { fromFs = fromJSON.fromFormatters, toFs = toJSON.toFormatters, fieldSet = fromJSON.fields, - subTypeNameList = Option(fromJSON.fromFormatters).map(_.getSubTypeNames).getOrElse(Nil) + subTypeNameList = Option(fromJSON.fromFormatters).map(_.getSerializedNames).getOrElse(Nil) ) def instance[A]( readFn: JValue => JValidation[A], writeFn: A => JValue, - fromFs: Formatters[FromJSON], - toFs: Formatters[ToJSON], + fromFs: FromFormatters, + toFs: ToFormatters, subTypeNameList: List[String] = Nil, - fieldSet: Set[String] = FromJSON.emptyFieldsSet): JSON[A] = new { - override def read(jval: JValue): JValidation[A] = readFn(jval) - override def write(value: A): JValue = writeFn(value) - override val fields: Set[String] = fieldSet - override def subTypeNames: List[String] = subTypeNameList - override val fromFormatters: Formatters[FromJSON] = fromFs - override val toFormatters: Formatters[ToJSON] = toFs - } + fieldSet: Set[String] = FromJSON.emptyFieldsSet): JSON[A] with TypeSelectorContainer = + new JSON[A] with TypeSelectorContainer { + override def read(jval: JValue): JValidation[A] = readFn(jval) + override def write(value: A): JValue = writeFn(value) + override val fields: Set[String] = fieldSet + override def subTypeNames: List[String] = subTypeNameList + override val fromFormatters: FromFormatters = fromFs + override val toFormatters: ToFormatters = toFs + + override def typeSelectors: List[TypeSelector] = ??? + } +} + +// Compatibility with Scala 2 syntax +// provide merging +case class TypeSelector(json: JSON[_]) {} +trait TypeSelectorContainer { + def typeSelectors: List[TypeSelector] } class JSONException(msg: String) extends RuntimeException(msg) diff --git a/json/json-core/src/main/scala-3/io/sphere/json/ToJSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/ToJSON.scala index 57ececc1..1cb77010 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/ToJSON.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/ToJSON.scala @@ -1,6 +1,6 @@ package io.sphere.json -import io.sphere.json.generic.JSONTypeSwitch.Formatters +import io.sphere.json.generic.JSONTypeSwitch.ToFormatters import org.json4s.JsonAST.JValue /** Type class for types that can be written to JSON. */ @@ -8,7 +8,7 @@ trait ToJSON[A] extends Serializable { def write(value: A): JValue // Filled automatically for traits - val toFormatters: Formatters[ToJSON] = null + val toFormatters: ToFormatters = null } class JSONWriteException(msg: String) extends JSONException(msg) @@ -19,8 +19,9 @@ object ToJSON extends ToJSONInstances with ToJSONCatsInstances with generic.Deri /** construct an instance from a function */ - def instance[T](toJson: T => JValue, toFs: Formatters[ToJSON] = null): ToJSON[T] = new ToJSON[T] { + def instance[T](toJson: T => JValue, toFs: ToFormatters = null): ToJSON[T] = new ToJSON[T] { override def write(value: T): JValue = toJson(value) - override val toFormatters: Formatters[ToJSON] = toFs + + override val toFormatters: ToFormatters = toFs } } diff --git a/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala index e8a5a4a4..4ee11b5c 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala @@ -6,32 +6,31 @@ import org.json4s.DefaultJsonFormats.given import org.json4s.{JObject, JString, jvalue2monadic, jvalue2readerSyntax} import scala.compiletime.{constValue, constValueTuple} +import scala.reflect.ClassTag object JSONTypeSwitch { import scala.compiletime.{erasedValue, summonInline} - inline def deriveToFormatters[SuperType, SubTypes <: Tuple]: Formatters[ToJSON] = { + inline def deriveToFormatters[SuperType, SubTypes <: Tuple]: ToFormatters = { val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] summonToFormatters[SubTypes]() - .reduce(Formatters.merge) + .reduce(ToFormatters.merge) .copy(typeDiscriminator = traitMetaData.typeDiscriminator) - .addTypeNames(traitMetaData.serializedNamesOfSubTypes) } - inline def deriveFromFormatters[SuperType, SubTypes <: Tuple]: Formatters[FromJSON] = { + inline def deriveFromFormatters[SuperType, SubTypes <: Tuple]: FromFormatters = { val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] summonFromFormatters[SubTypes]() - .reduce(Formatters.merge) + .reduce(FromFormatters.merge) .copy(typeDiscriminator = traitMetaData.typeDiscriminator) - .addTypeNames(traitMetaData.serializedNamesOfSubTypes) } - inline def toJsonTypeSwitch[SuperType](formatters: Formatters[ToJSON]): ToJSON[SuperType] = + inline def toJsonTypeSwitch[SuperType](formatters: ToFormatters): ToJSON[SuperType] = ToJSON.instance( toJson = { scalaValue => - val scalaTypeName = scalaValue.asInstanceOf[Product].productPrefix - val serializedTypeName = formatters.serializedTypeNames(scalaTypeName) - val jsonObj = formatters.forCaseClasses(scalaTypeName).write(scalaValue) match { + val clazz = scalaValue.getClass + val serializedTypeName = formatters.serializedTypeNames(clazz) + val jsonObj = formatters.formatterByClass(clazz).write(scalaValue) match { case JObject(obj) => obj case json => throw new Exception(s"This code only handles objects as of now, but got: $json") @@ -42,22 +41,21 @@ object JSONTypeSwitch { toFs = formatters ) - inline def fromJsonTypeSwitch[SuperType]( - formatters: Formatters[FromJSON]): FromJSON[SuperType] = { - val scalaTypeNames = formatters.serializedTypeNames.map((on, n) => (n, on)) - + inline def fromJsonTypeSwitch[SuperType](formatters: FromFormatters): FromJSON[SuperType] = FromJSON.instance( readFn = { case jObject: JObject => val serializedTypeName = (jObject \ formatters.typeDiscriminator).as[String] - val scalaTypeName = scalaTypeNames(serializedTypeName) - formatters.forCaseClasses(scalaTypeName).read(jObject).map(_.asInstanceOf[SuperType]) + val scalaTypeName = formatters.scalaNamesFromSerializedNames(serializedTypeName) + formatters + .formatterByScalaName(scalaTypeName) + .read(jObject) + .map(_.asInstanceOf[SuperType]) case x => Validated.invalidNel(JSONParseError(s"JSON object expected. Got: '$x'")) }, fromFs = formatters ) - } inline def jsonTypeSwitch[SuperType, SubTypes <: Tuple]: JSON[SuperType] = { val fromFormatters = deriveFromFormatters[SuperType, SubTypes] @@ -68,7 +66,7 @@ object JSONTypeSwitch { JSON.instance( writeFn = toJson.write, readFn = fromJson.read, - subTypeNameList = fromFormatters.getSubTypeNames, + subTypeNameList = fromFormatters.getSerializedNames, fromFs = fromJson.fromFormatters, toFs = toJson.toFormatters ) @@ -76,68 +74,96 @@ object JSONTypeSwitch { inline private def summonFromFormatters[T <: Tuple]( d: Int = 0, - acc: Vector[Formatters[FromJSON]] = Vector.empty): Vector[Formatters[FromJSON]] = + acc: Vector[FromFormatters] = Vector.empty): Vector[FromFormatters] = inline erasedValue[T] match { case _: EmptyTuple => acc case _: (t *: ts) => val traitMetaData = AnnotationReader.readTraitMetaData[t] val headFormatter = summonInline[FromJSON[t]].asInstanceOf[FromJSON[Any]] - val formatterMap = - if (traitMetaData.isTrait) { - headFormatter.fromFormatters.forCaseClasses - } else - Map(traitMetaData.top.scalaName -> headFormatter) - - val f = Formatters[FromJSON]( - serializedTypeNames = traitMetaData.serializedNamesOfSubTypes, - forCaseClasses = formatterMap, + val (formatterMap, nameMap) = + if (traitMetaData.isTrait) + ( + headFormatter.fromFormatters.formatterByScalaName, + headFormatter.fromFormatters.scalaNamesFromSerializedNames) + else + ( + Map(traitMetaData.top.scalaName -> headFormatter), + Map(traitMetaData.top.serializedName -> traitMetaData.top.scalaName) + ) + + val f = FromFormatters( + scalaNamesFromSerializedNames = nameMap, + formatterByScalaName = formatterMap, typeDiscriminator = traitMetaData.typeDiscriminator ) summonFromFormatters[ts](d + 1, acc :+ f) } inline private def summonToFormatters[T <: Tuple]( - acc: Vector[Formatters[ToJSON]] = Vector.empty): Vector[Formatters[ToJSON]] = + acc: Vector[ToFormatters] = Vector.empty): Vector[ToFormatters] = inline erasedValue[T] match { case _: EmptyTuple => acc case _: (t *: ts) => val traitMetaData = AnnotationReader.readTraitMetaData[t] - val headFormatter = summonInline[ToJSON[t]].asInstanceOf[ToJSON[Any]] - val formatterMap = - if (traitMetaData.isTrait) - headFormatter.toFormatters.forCaseClasses - else - Map(traitMetaData.top.scalaName -> headFormatter) + val formatterT = summonInline[ToJSON[t]].asInstanceOf[ToJSON[Any]] - val f = Formatters[ToJSON]( - serializedTypeNames = traitMetaData.serializedNamesOfSubTypes, - forCaseClasses = formatterMap, + val (formatterMap, serializedTypeNames) = + if (traitMetaData.isTrait) + ( + formatterT.toFormatters.formatterByClass, + formatterT.toFormatters.serializedTypeNames + ) + else { + val clazz = summonInline[ClassTag[t]].runtimeClass + ( + Map(clazz -> formatterT), + Map(clazz -> traitMetaData.top.serializedName) + ) + } + + val f = ToFormatters( + serializedTypeNames = serializedTypeNames, + formatterByClass = formatterMap, typeDiscriminator = traitMetaData.typeDiscriminator ) summonToFormatters[ts](acc :+ f) } - case class Formatters[JsonKind[_]]( - serializedTypeNames: Map[String, String], - forCaseClasses: Map[String, JsonKind[Any]], + case class ToFormatters( + serializedTypeNames: Map[Class[_], String], + formatterByClass: Map[Class[_], ToJSON[Any]], typeDiscriminator: String - ) { - def addTypeNames(names: Map[String, String]): Formatters[JsonKind] = - copy(serializedTypeNames = serializedTypeNames ++ names) + ) + object ToFormatters { + def merge(f1: ToFormatters, f2: ToFormatters): ToFormatters = { + require( + f1.typeDiscriminator == f2.typeDiscriminator, + "@JSONTypeHintField has to be the same on all traits") + ToFormatters( + serializedTypeNames = f1.serializedTypeNames ++ f2.serializedTypeNames, + formatterByClass = f1.formatterByClass ++ f2.formatterByClass, + typeDiscriminator = f1.typeDiscriminator + ) + } + } - def getSubTypeNames: List[String] = serializedTypeNames.values.toList + case class FromFormatters( + scalaNamesFromSerializedNames: Map[String, String], + formatterByScalaName: Map[String, FromJSON[Any]], + typeDiscriminator: String + ) { + def getSerializedNames: List[String] = scalaNamesFromSerializedNames.keys.toList } - object Formatters { - def merge[JsonKind[_]]( - f1: Formatters[JsonKind], - f2: Formatters[JsonKind]): Formatters[JsonKind] = { + object FromFormatters { + def merge(f1: FromFormatters, f2: FromFormatters): FromFormatters = { require( f1.typeDiscriminator == f2.typeDiscriminator, "@JSONTypeHintField has to be the same on all traits") - Formatters[JsonKind]( - serializedTypeNames = f1.serializedTypeNames ++ f2.serializedTypeNames, - forCaseClasses = f1.forCaseClasses ++ f2.forCaseClasses, + FromFormatters( + scalaNamesFromSerializedNames = + f1.scalaNamesFromSerializedNames ++ f2.scalaNamesFromSerializedNames, + formatterByScalaName = f1.formatterByScalaName ++ f2.formatterByScalaName, typeDiscriminator = f1.typeDiscriminator ) } diff --git a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala index 63f496cf..9264d232 100644 --- a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala +++ b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala @@ -97,27 +97,57 @@ class JsonTypeSwitchSpec extends AnyWordSpec with Matchers { } } - "handle PlatformFormattedNotification case" in { + "handle the PlatformFormattedNotification case " when { + // This means deriving a formatter for a supertrait that has 3 sub traits. + // 1 of them passed in as a type parameter and fully being derived + // the other 2 sub traits passed in as a value parameter, through their combined type selectors + + "using the /old/ syntax" in { + +// val formatSub2 = jsonTypeSwitch[SubTrait2, SubTrait2.O3.type, SubTrait2.O4.type](Nil) +// val formatSub3 = jsonTypeSwitch[SubTrait3, SubTrait3.O5.type, SubTrait3.O6.type](Nil) +// +// val formatSuper: JSON[SuperTrait] = jsonTypeSwitch[SuperTrait, SubTrait1]() +// +// val objs = +// List[SuperTrait]( +// SubTrait1.O1, +// SubTrait1.O2, +// SubTrait2.O3, +// SubTrait2.O4, +// SubTrait3.O5, +// SubTrait3.O6, +// SubTrait4.O7) +// +// val res = objs.map(formatSuper.write).map(formatSuper.read).sequence.getOrElse(null) +// +// res must be(objs) - type Trait234 = SubTrait2 *: (SubTrait3, SubTrait4) + } + + "using the /new/ syntax" in { + type Trait234 = SubTrait2 *: (SubTrait3, SubTrait4) - val formatSuper: JSON[SuperTrait] = jsonTypeSwitch[SuperTrait, SubTrait1 *: Trait234] + val formatSuper: JSON[SuperTrait] = jsonTypeSwitch[SuperTrait, SubTrait1 *: Trait234] - val objs = - List[SuperTrait]( - SubTrait1.O1, - SubTrait1.O2, - SubTrait2.O3, - SubTrait2.O4, - SubTrait3.O5, - SubTrait3.O6, - SubTrait4.O7) + val objs = + List[SuperTrait]( + SubTrait1.O1, + SubTrait1.O2, + SubTrait2.O3, + SubTrait2.O4, + SubTrait3.O5, + SubTrait3.O6, + SubTrait4.O7) - val res = objs.map(formatSuper.write).map(formatSuper.read).sequence.getOrElse(null) + val res = objs.map(formatSuper.write).map(formatSuper.read).sequence.getOrElse(null) - res must be(objs) + res must be(objs) + + } } + } def check[A](a: A, json: String)(using format: JSON[A]): Unit = { diff --git a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/SubTypeNameSpec.scala b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/SubTypeNameSpec.scala index 28165f04..75f6d26b 100644 --- a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/SubTypeNameSpec.scala +++ b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/SubTypeNameSpec.scala @@ -35,8 +35,8 @@ class SubTypeNameSpec extends AnyWordSpec with Matchers { val format: JSON[SuperType2] = jsonTypeSwitch[SuperType2, (SubType1, SubType2)] - val names = - List("SubClass1A", "SubClass2A", "SubType1", "SubType2") + // Should only contain class names no trait names + val names = List("SubClass1A", "SubClass2A") format.subTypeNames must be(names) } } diff --git a/util/src/main/scala-3/AnnotationReader.scala b/util/src/main/scala-3/AnnotationReader.scala index 2b53a4fd..236b64de 100644 --- a/util/src/main/scala-3/AnnotationReader.scala +++ b/util/src/main/scala-3/AnnotationReader.scala @@ -18,6 +18,8 @@ case class TypeMetaData( ) { val typeHint: Option[String] = typeHintRaw.filterNot(_.toList.forall(_ == ' ')) + + val serializedName: String = typeHint.getOrElse(scalaName) } /** This class also works for case classes not only traits, in case of case classes only the `top` From 17d506e7e20e5a1a62987418e488fd8849d2d47a Mon Sep 17 00:00:00 2001 From: benkobalog Date: Fri, 25 Jul 2025 12:35:58 +0200 Subject: [PATCH 139/142] Use Class instead of String for ToFormatters --- .../test/scala-3/io/sphere/json/generic/SubTypeNameSpec.scala | 2 +- util/src/main/scala-3/AnnotationReader.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/SubTypeNameSpec.scala b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/SubTypeNameSpec.scala index 75f6d26b..39a175e7 100644 --- a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/SubTypeNameSpec.scala +++ b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/SubTypeNameSpec.scala @@ -35,7 +35,7 @@ class SubTypeNameSpec extends AnyWordSpec with Matchers { val format: JSON[SuperType2] = jsonTypeSwitch[SuperType2, (SubType1, SubType2)] - // Should only contain class names no trait names + // Should only contain class names, no trait names val names = List("SubClass1A", "SubClass2A") format.subTypeNames must be(names) } diff --git a/util/src/main/scala-3/AnnotationReader.scala b/util/src/main/scala-3/AnnotationReader.scala index 236b64de..f5a09429 100644 --- a/util/src/main/scala-3/AnnotationReader.scala +++ b/util/src/main/scala-3/AnnotationReader.scala @@ -18,7 +18,7 @@ case class TypeMetaData( ) { val typeHint: Option[String] = typeHintRaw.filterNot(_.toList.forall(_ == ' ')) - + val serializedName: String = typeHint.getOrElse(scalaName) } From 824767f4ab4552593c91283b7d9020b0eae508ed Mon Sep 17 00:00:00 2001 From: benkobalog Date: Fri, 25 Jul 2025 14:41:53 +0200 Subject: [PATCH 140/142] Improve naming for ToFormatters/FromFormatters fields --- .../main/scala-3/io/sphere/json/JSON.scala | 9 ++-- .../sphere/json/generic/JSONTypeSwitch.scala | 45 +++++++++---------- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala index 6aad621e..f4cf19d5 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala @@ -7,7 +7,7 @@ import org.json4s.JsonAST.JValue trait JSON[A] extends FromJSON[A] with ToJSON[A] { // This field is only used in case we derive a trait, for classes/objects it remains empty // It uses the JSON names not the Scala names (if there's @JSONTypeHint renaming a class the renamed name is used here) - def subTypeNames: List[String] = Nil + def subTypeNames: Vector[String] = Vector.empty } object JSON extends JSONCatsInstances { @@ -19,7 +19,8 @@ object JSON extends JSONCatsInstances { fromFs = fromJSON.fromFormatters, toFs = toJSON.toFormatters, fieldSet = fromJSON.fields, - subTypeNameList = Option(fromJSON.fromFormatters).map(_.getSerializedNames).getOrElse(Nil) + subTypeNameList = + Option(fromJSON.fromFormatters).map(_.serializedNames).getOrElse(Vector.empty) ) def instance[A]( @@ -27,13 +28,13 @@ object JSON extends JSONCatsInstances { writeFn: A => JValue, fromFs: FromFormatters, toFs: ToFormatters, - subTypeNameList: List[String] = Nil, + subTypeNameList: Vector[String] = Vector.empty, fieldSet: Set[String] = FromJSON.emptyFieldsSet): JSON[A] with TypeSelectorContainer = new JSON[A] with TypeSelectorContainer { override def read(jval: JValue): JValidation[A] = readFn(jval) override def write(value: A): JValue = writeFn(value) override val fields: Set[String] = fieldSet - override def subTypeNames: List[String] = subTypeNameList + override def subTypeNames: Vector[String] = subTypeNameList override val fromFormatters: FromFormatters = fromFs override val toFormatters: ToFormatters = toFs diff --git a/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala index 4ee11b5c..3b0763fc 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala @@ -29,7 +29,7 @@ object JSONTypeSwitch { ToJSON.instance( toJson = { scalaValue => val clazz = scalaValue.getClass - val serializedTypeName = formatters.serializedTypeNames(clazz) + val serializedTypeName = formatters.serializedNamesByClass(clazz) val jsonObj = formatters.formatterByClass(clazz).write(scalaValue) match { case JObject(obj) => obj case json => @@ -46,9 +46,8 @@ object JSONTypeSwitch { readFn = { case jObject: JObject => val serializedTypeName = (jObject \ formatters.typeDiscriminator).as[String] - val scalaTypeName = formatters.scalaNamesFromSerializedNames(serializedTypeName) formatters - .formatterByScalaName(scalaTypeName) + .formatterBySerializedName(serializedTypeName) .read(jObject) .map(_.asInstanceOf[SuperType]) case x => @@ -66,37 +65,36 @@ object JSONTypeSwitch { JSON.instance( writeFn = toJson.write, readFn = fromJson.read, - subTypeNameList = fromFormatters.getSerializedNames, + subTypeNameList = fromFormatters.serializedNames, fromFs = fromJson.fromFormatters, toFs = toJson.toFormatters ) } inline private def summonFromFormatters[T <: Tuple]( - d: Int = 0, acc: Vector[FromFormatters] = Vector.empty): Vector[FromFormatters] = inline erasedValue[T] match { case _: EmptyTuple => acc case _: (t *: ts) => val traitMetaData = AnnotationReader.readTraitMetaData[t] val headFormatter = summonInline[FromJSON[t]].asInstanceOf[FromJSON[Any]] - val (formatterMap, nameMap) = + val (formatterMap, names) = if (traitMetaData.isTrait) ( - headFormatter.fromFormatters.formatterByScalaName, - headFormatter.fromFormatters.scalaNamesFromSerializedNames) + headFormatter.fromFormatters.formatterBySerializedName, + headFormatter.fromFormatters.serializedNames) else ( - Map(traitMetaData.top.scalaName -> headFormatter), - Map(traitMetaData.top.serializedName -> traitMetaData.top.scalaName) + Map(traitMetaData.top.serializedName -> headFormatter), + Vector(traitMetaData.top.serializedName) ) val f = FromFormatters( - scalaNamesFromSerializedNames = nameMap, - formatterByScalaName = formatterMap, + serializedNames = names, + formatterBySerializedName = formatterMap, typeDiscriminator = traitMetaData.typeDiscriminator ) - summonFromFormatters[ts](d + 1, acc :+ f) + summonFromFormatters[ts](acc :+ f) } inline private def summonToFormatters[T <: Tuple]( @@ -111,7 +109,7 @@ object JSONTypeSwitch { if (traitMetaData.isTrait) ( formatterT.toFormatters.formatterByClass, - formatterT.toFormatters.serializedTypeNames + formatterT.toFormatters.serializedNamesByClass ) else { val clazz = summonInline[ClassTag[t]].runtimeClass @@ -122,7 +120,7 @@ object JSONTypeSwitch { } val f = ToFormatters( - serializedTypeNames = serializedTypeNames, + serializedNamesByClass = serializedTypeNames, formatterByClass = formatterMap, typeDiscriminator = traitMetaData.typeDiscriminator ) @@ -130,7 +128,7 @@ object JSONTypeSwitch { } case class ToFormatters( - serializedTypeNames: Map[Class[_], String], + serializedNamesByClass: Map[Class[_], String], formatterByClass: Map[Class[_], ToJSON[Any]], typeDiscriminator: String ) @@ -140,7 +138,7 @@ object JSONTypeSwitch { f1.typeDiscriminator == f2.typeDiscriminator, "@JSONTypeHintField has to be the same on all traits") ToFormatters( - serializedTypeNames = f1.serializedTypeNames ++ f2.serializedTypeNames, + serializedNamesByClass = f1.serializedNamesByClass ++ f2.serializedNamesByClass, formatterByClass = f1.formatterByClass ++ f2.formatterByClass, typeDiscriminator = f1.typeDiscriminator ) @@ -148,12 +146,10 @@ object JSONTypeSwitch { } case class FromFormatters( - scalaNamesFromSerializedNames: Map[String, String], - formatterByScalaName: Map[String, FromJSON[Any]], + serializedNames: Vector[String], + formatterBySerializedName: Map[String, FromJSON[Any]], typeDiscriminator: String - ) { - def getSerializedNames: List[String] = scalaNamesFromSerializedNames.keys.toList - } + ) object FromFormatters { def merge(f1: FromFormatters, f2: FromFormatters): FromFormatters = { @@ -161,9 +157,8 @@ object JSONTypeSwitch { f1.typeDiscriminator == f2.typeDiscriminator, "@JSONTypeHintField has to be the same on all traits") FromFormatters( - scalaNamesFromSerializedNames = - f1.scalaNamesFromSerializedNames ++ f2.scalaNamesFromSerializedNames, - formatterByScalaName = f1.formatterByScalaName ++ f2.formatterByScalaName, + serializedNames = f1.serializedNames ++ f2.serializedNames, + formatterBySerializedName = f1.formatterBySerializedName ++ f2.formatterBySerializedName, typeDiscriminator = f1.typeDiscriminator ) } From 2c04a09fe210966733ff81107221b2eda20c629a Mon Sep 17 00:00:00 2001 From: benkobalog Date: Fri, 25 Jul 2025 14:54:51 +0200 Subject: [PATCH 141/142] Minor refactor --- .../scala-3/io/sphere/json/FromJSON.scala | 3 +++ .../main/scala-3/io/sphere/json/JSON.scala | 3 +-- .../main/scala-3/io/sphere/json/ToJSON.scala | 4 ++++ .../sphere/json/generic/JSONTypeSwitch.scala | 21 ++++++++++--------- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/json/json-core/src/main/scala-3/io/sphere/json/FromJSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/FromJSON.scala index e1111c56..2029ed11 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/FromJSON.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/FromJSON.scala @@ -14,6 +14,9 @@ trait FromJSON[A] extends Serializable { // This is automatically filled for traits val fromFormatters: FromFormatters = null + def getSerializedNames: Vector[String] = + if (fromFormatters == null) Vector.empty + else fromFormatters.serializedNames } object FromJSON extends FromJSONInstances with FromJSONCatsInstances with generic.DeriveFromJSON { diff --git a/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala index f4cf19d5..02e869d9 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala @@ -19,8 +19,7 @@ object JSON extends JSONCatsInstances { fromFs = fromJSON.fromFormatters, toFs = toJSON.toFormatters, fieldSet = fromJSON.fields, - subTypeNameList = - Option(fromJSON.fromFormatters).map(_.serializedNames).getOrElse(Vector.empty) + subTypeNameList = fromJSON.getSerializedNames ) def instance[A]( diff --git a/json/json-core/src/main/scala-3/io/sphere/json/ToJSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/ToJSON.scala index 1cb77010..d9e12d05 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/ToJSON.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/ToJSON.scala @@ -8,6 +8,10 @@ trait ToJSON[A] extends Serializable { def write(value: A): JValue // Filled automatically for traits + // I decided to not use option, because it's not an internal type anyway and + // on traits it's always filled + // on case classes it's always null + // So there's not a lot of reasons to check for it runtime in most cases. val toFormatters: ToFormatters = null } diff --git a/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala index 3b0763fc..562fed2f 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/generic/JSONTypeSwitch.scala @@ -14,15 +14,13 @@ object JSONTypeSwitch { inline def deriveToFormatters[SuperType, SubTypes <: Tuple]: ToFormatters = { val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] summonToFormatters[SubTypes]() - .reduce(ToFormatters.merge) - .copy(typeDiscriminator = traitMetaData.typeDiscriminator) + .reduce(ToFormatters.merge(traitMetaData.typeDiscriminator)) } inline def deriveFromFormatters[SuperType, SubTypes <: Tuple]: FromFormatters = { val traitMetaData = AnnotationReader.readTraitMetaData[SuperType] summonFromFormatters[SubTypes]() - .reduce(FromFormatters.merge) - .copy(typeDiscriminator = traitMetaData.typeDiscriminator) + .reduce(FromFormatters.merge(traitMetaData.typeDiscriminator)) } inline def toJsonTypeSwitch[SuperType](formatters: ToFormatters): ToJSON[SuperType] = @@ -133,14 +131,15 @@ object JSONTypeSwitch { typeDiscriminator: String ) object ToFormatters { - def merge(f1: ToFormatters, f2: ToFormatters): ToFormatters = { + def merge( + typeDiscriminatorFromParent: String)(f1: ToFormatters, f2: ToFormatters): ToFormatters = { require( - f1.typeDiscriminator == f2.typeDiscriminator, + f1.typeDiscriminator == f2.typeDiscriminator && typeDiscriminatorFromParent == f2.typeDiscriminator, "@JSONTypeHintField has to be the same on all traits") ToFormatters( serializedNamesByClass = f1.serializedNamesByClass ++ f2.serializedNamesByClass, formatterByClass = f1.formatterByClass ++ f2.formatterByClass, - typeDiscriminator = f1.typeDiscriminator + typeDiscriminator = typeDiscriminatorFromParent ) } } @@ -152,14 +151,16 @@ object JSONTypeSwitch { ) object FromFormatters { - def merge(f1: FromFormatters, f2: FromFormatters): FromFormatters = { + def merge(typeDiscriminatorFromParent: String)( + f1: FromFormatters, + f2: FromFormatters): FromFormatters = { require( - f1.typeDiscriminator == f2.typeDiscriminator, + f1.typeDiscriminator == f2.typeDiscriminator && typeDiscriminatorFromParent == f2.typeDiscriminator, "@JSONTypeHintField has to be the same on all traits") FromFormatters( serializedNames = f1.serializedNames ++ f2.serializedNames, formatterBySerializedName = f1.formatterBySerializedName ++ f2.formatterBySerializedName, - typeDiscriminator = f1.typeDiscriminator + typeDiscriminator = typeDiscriminatorFromParent ) } } From 59d559f511ebaa6438a95590cf59b39920f347ba Mon Sep 17 00:00:00 2001 From: benkobalog Date: Fri, 25 Jul 2025 20:11:20 +0200 Subject: [PATCH 142/142] PlatformFormattedNotification case supported --- .../main/scala-3/io/sphere/json/JSON.scala | 13 +- .../io/sphere/json/generic/generic.scala | 152 +++++++++++------- .../json/generic/JsonTypeSwitchSpec.scala | 37 +++-- 3 files changed, 116 insertions(+), 86 deletions(-) diff --git a/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala b/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala index 02e869d9..d1c4f9a2 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/JSON.scala @@ -28,26 +28,17 @@ object JSON extends JSONCatsInstances { fromFs: FromFormatters, toFs: ToFormatters, subTypeNameList: Vector[String] = Vector.empty, - fieldSet: Set[String] = FromJSON.emptyFieldsSet): JSON[A] with TypeSelectorContainer = - new JSON[A] with TypeSelectorContainer { + fieldSet: Set[String] = FromJSON.emptyFieldsSet): JSON[A] = + new JSON[A] { override def read(jval: JValue): JValidation[A] = readFn(jval) override def write(value: A): JValue = writeFn(value) override val fields: Set[String] = fieldSet override def subTypeNames: Vector[String] = subTypeNameList override val fromFormatters: FromFormatters = fromFs override val toFormatters: ToFormatters = toFs - - override def typeSelectors: List[TypeSelector] = ??? } } -// Compatibility with Scala 2 syntax -// provide merging -case class TypeSelector(json: JSON[_]) {} -trait TypeSelectorContainer { - def typeSelectors: List[TypeSelector] -} - class JSONException(msg: String) extends RuntimeException(msg) sealed abstract class JSONError diff --git a/json/json-core/src/main/scala-3/io/sphere/json/generic/generic.scala b/json/json-core/src/main/scala-3/io/sphere/json/generic/generic.scala index 547c129d..b9ea67ef 100644 --- a/json/json-core/src/main/scala-3/io/sphere/json/generic/generic.scala +++ b/json/json-core/src/main/scala-3/io/sphere/json/generic/generic.scala @@ -1,6 +1,8 @@ package io.sphere.json.generic import io.sphere.json.* +import io.sphere.json.generic.JSONTypeSwitch.{FromFormatters, ToFormatters} +import org.json4s.JsonAST.JValue import scala.deriving.Mirror @@ -36,65 +38,103 @@ inline def fromJsonTypeSwitch[SuperType, SubTypes <: Tuple]: FromJSON[SuperType] // Compatibility with the scala-2 methods, that will be deprecated later // fromJsonTypeSwitch is not used, so no old syntax support will be added for now +// Compatibility with Scala 2 syntax +case class TypeSelector(clazz: Class[?], typeValue: String, json: JSON[?]) + +// Compatibility with Scala 2 syntax +trait TypeSelectorContainer { + def typeSelectors: List[TypeSelector] +} + +// Compatibility with Scala 2 syntax +private def addTypeSelectorContainer[A](derviedJson: JSON[A])( + typeSelectors: List[TypeSelector]): JSON[A] with TypeSelectorContainer = { + val additionalJsons = typeSelectors.map(_.json) + val mergedFromFormatters = additionalJsons + .map(_.fromFormatters) + .fold(derviedJson.fromFormatters)( + FromFormatters.merge(derviedJson.fromFormatters.typeDiscriminator)) + val mergedToFormatters = additionalJsons + .map(_.toFormatters) + .fold(derviedJson.toFormatters)(ToFormatters.merge(derviedJson.toFormatters.typeDiscriminator)) + + // We create the write/read methods again with the all the formatters present, so they have the correct references + val toJson = JSONTypeSwitch.toJsonTypeSwitch[A](mergedToFormatters) + val fromJson = JSONTypeSwitch.fromJsonTypeSwitch[A](mergedFromFormatters) + + new JSON[A] with TypeSelectorContainer { + override def read(jval: JValue): JValidation[A] = fromJson.read(jval) + override def write(value: A): JValue = toJson.write(value) + override val fields: Set[String] = fromJson.fields + override def subTypeNames: Vector[String] = Vector.empty + override val fromFormatters: FromFormatters = mergedFromFormatters + override val toFormatters: ToFormatters = mergedToFormatters + + override def typeSelectors: List[TypeSelector] = + toFormatters.serializedNamesByClass + .map((cls, serializedName) => TypeSelector(cls, serializedName, this)) + .toList + } +} // jsonTypeSwitch is used up to 26 parameters, so I'll up to 28 // format: off -inline def jsonTypeSwitch[SuperType, A1: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, Tuple1[A1]] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON, A21: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON, A21: JSON, A22: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON, A21: JSON, A22: JSON, A23: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22, A23)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON, A21: JSON, A22: JSON, A23: JSON, A24: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22, A23, A24)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON, A21: JSON, A22: JSON, A23: JSON, A24: JSON, A25: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22, A23, A24, A25)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON, A21: JSON, A22: JSON, A23: JSON, A24: JSON, A25: JSON, A26: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22, A23, A24, A25, A26)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON, A21: JSON, A22: JSON, A23: JSON, A24: JSON, A25: JSON, A26: JSON, A27: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22, A23, A24, A25, A26, A27)] -inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON, A21: JSON, A22: JSON, A23: JSON, A24: JSON, A25: JSON, A26: JSON, A27: JSON, A28: JSON](ignoredList: List[Nothing]): JSON[SuperType] = - jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22, A23, A24, A25, A26, A27, A28)] +inline def jsonTypeSwitch[SuperType, A1: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, Tuple1[A1]])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON, A21: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON, A21: JSON, A22: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON, A21: JSON, A22: JSON, A23: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22, A23)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON, A21: JSON, A22: JSON, A23: JSON, A24: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22, A23, A24)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON, A21: JSON, A22: JSON, A23: JSON, A24: JSON, A25: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22, A23, A24, A25)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON, A21: JSON, A22: JSON, A23: JSON, A24: JSON, A25: JSON, A26: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22, A23, A24, A25, A26)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON, A21: JSON, A22: JSON, A23: JSON, A24: JSON, A25: JSON, A26: JSON, A27: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22, A23, A24, A25, A26, A27)])(typeSelectors) +inline def jsonTypeSwitch[SuperType, A1: JSON, A2: JSON, A3: JSON, A4: JSON, A5: JSON, A6: JSON, A7: JSON, A8: JSON, A9: JSON, A10: JSON, A11: JSON, A12: JSON, A13: JSON, A14: JSON, A15: JSON, A16: JSON, A17: JSON, A18: JSON, A19: JSON, A20: JSON, A21: JSON, A22: JSON, A23: JSON, A24: JSON, A25: JSON, A26: JSON, A27: JSON, A28: JSON](typeSelectors: List[TypeSelector]): JSON[SuperType] with TypeSelectorContainer = + addTypeSelectorContainer(jsonTypeSwitch[SuperType, (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22, A23, A24, A25, A26, A27, A28)])(typeSelectors) // format: on // toJsonTypeSwitch is used up to 5 parameters, so I'll add up to 7 diff --git a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala index 9264d232..8de9a390 100644 --- a/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala +++ b/json/json-derivation/src/test/scala-3/io/sphere/json/generic/JsonTypeSwitchSpec.scala @@ -97,31 +97,30 @@ class JsonTypeSwitchSpec extends AnyWordSpec with Matchers { } } - "handle the PlatformFormattedNotification case " when { + "handle the PlatformFormattedNotification case" when { // This means deriving a formatter for a supertrait that has 3 sub traits. // 1 of them passed in as a type parameter and fully being derived // the other 2 sub traits passed in as a value parameter, through their combined type selectors "using the /old/ syntax" in { + val formatSub2 = jsonTypeSwitch[SubTrait2, SubTrait2.O3.type, SubTrait2.O4.type](Nil) + val formatSub3 = jsonTypeSwitch[SubTrait3, SubTrait3.O5.type, SubTrait3.O6.type](Nil) -// val formatSub2 = jsonTypeSwitch[SubTrait2, SubTrait2.O3.type, SubTrait2.O4.type](Nil) -// val formatSub3 = jsonTypeSwitch[SubTrait3, SubTrait3.O5.type, SubTrait3.O6.type](Nil) -// -// val formatSuper: JSON[SuperTrait] = jsonTypeSwitch[SuperTrait, SubTrait1]() -// -// val objs = -// List[SuperTrait]( -// SubTrait1.O1, -// SubTrait1.O2, -// SubTrait2.O3, -// SubTrait2.O4, -// SubTrait3.O5, -// SubTrait3.O6, -// SubTrait4.O7) -// -// val res = objs.map(formatSuper.write).map(formatSuper.read).sequence.getOrElse(null) -// -// res must be(objs) + val typeSelectors = formatSub2.typeSelectors ++ formatSub3.typeSelectors + val formatSuper: JSON[SuperTrait] = jsonTypeSwitch[SuperTrait, SubTrait1](typeSelectors) + + val objs = + List[SuperTrait]( + SubTrait1.O1, + SubTrait1.O2, + SubTrait2.O3, + SubTrait2.O4, + SubTrait3.O5, + SubTrait3.O6) + + val res = objs.map(formatSuper.write).map(formatSuper.read).sequence.getOrElse(null) + + res must be(objs) }