Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sphere-mongo-3 and sphere-json-3 (the scala 3 version of similarly named modules) #651

Open
wants to merge 74 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
ff4171a
sphere-mongo-derivation-scala-3
peter-empen Feb 21, 2024
6aab3f9
sphere-mongo-core, sphere-util compiles with both Scala 2 and 3
benko-ct Feb 22, 2024
91424e0
some macro test code
benko-ct Feb 22, 2024
04a2d75
some macro test code2
benko-ct Feb 22, 2024
bbabb8a
some macro test code2
benko-ct Feb 22, 2024
77787a9
some macro test code2
benko-ct Feb 22, 2024
33049ff
start module and case-class derivation
peter-empen Feb 23, 2024
4cee24d
Autoderivation example with fake bson
benko-ct Feb 26, 2024
3a120ca
brush up specs
peter-empen Feb 29, 2024
c888f4f
add .bsp to .gitignore
peter-empen Feb 29, 2024
cf41666
scalafmt
peter-empen Feb 29, 2024
4da664c
fix SBT warnings on target
peter-empen Mar 1, 2024
a8845d1
Port the initial implementation from FakeBson to Actual MongoDB types
benko-ct Apr 19, 2024
396417a
Add MongoKey and MongoEmbedded implementations
benko-ct Apr 29, 2024
dbee359
Add MongoTypeHint and MongoTypeHintField
benko-ct May 17, 2024
02388ea
ENE-49 Fix infinite loop in derivation
programaker May 23, 2024
9298ec3
ENE-49 Add a case with `implicit` keyword
programaker May 23, 2024
8ced215
add DefaultValuesSpec
peter-empen May 23, 2024
eccd881
Merge branch 'ENE-49__fix-derivation-infinite-loop' into scala-3
programaker May 23, 2024
e4ccd28
Fix OptionMongoFormatSpec
benko-ct May 23, 2024
244dd51
Refactor AnnotationReader macro functions and Remove bad imports.
benko-ct May 23, 2024
cccf067
Move TypedMongoFormat instances to DefaultMongoFormats
benko-ct May 23, 2024
e19d164
ENE-49 Enums work
programaker May 23, 2024
363d3e7
Merge branch 'ENE-49__dev' into scala-3
programaker May 23, 2024
2f5d1eb
Support default values
benko-ct May 23, 2024
1eb13a2
ENE-49 Solve merge conflicts
programaker May 23, 2024
e2b478a
ENE-49 Spec for class with fields with default value
programaker May 23, 2024
aecda6e
Add DeriveMongoFormatSpec
benko-ct May 23, 2024
67943d7
brushing up some code
peter-empen May 24, 2024
ba48367
stat with MongoIgnore
peter-empen May 24, 2024
1b85335
ENE-49 Porting Json Lib - Initial version
ysedira May 24, 2024
8f2d38e
Fix tests
benko-ct May 24, 2024
991644c
toMongoValue with MongoIgnore
peter-empen May 24, 2024
722edac
Fix some Option/Null behaviour
benko-ct Jun 17, 2024
425bc8d
remove warnings
benko-ct Jun 17, 2024
e5dc7f4
Fix sumtype writer
benko-ct Jun 17, 2024
0796166
change implicits to given
benko-ct Jun 17, 2024
76e0073
remove forProduct spec because it was only testing the internal detai…
benko-ct Jun 17, 2024
62a6b76
Add deriveSingletonJSON
benko-ct Jun 18, 2024
0294477
sphere-json-core now compiles with Scala3
benko-ct Jul 1, 2024
7bf1515
Turn off indentation based syntax.
benko-ct Jul 19, 2024
f4311e6
Turn off indentation based syntax.
benko-ct Jul 19, 2024
f2cbf08
Move sphere-json-derivation-3 to sphere-json-core (just the main, not…
benko-ct Jul 20, 2024
185d1c7
Merge branch 'refs/heads/main' into scala-3
benko-ct Aug 15, 2024
7b84b3c
move mongo-derivation-3 to mongo-3, separating the MongoFormat implem…
benko-ct Nov 22, 2024
3a509a5
Port DefaultMongoFormats to mongo-3
benko-ct Nov 25, 2024
bd03551
Add BaseMoneyMongoFormatTest
benko-ct Nov 25, 2024
a8490a8
Remove "New anonymous class definition will be duplicated at each inl…
benko-ct Nov 25, 2024
5c8db69
Revert "Remove "New anonymous class definition will be duplicated at …
benko-ct Nov 25, 2024
52e5e05
Small refactor
benko-ct Nov 25, 2024
4eb3676
Small refactor
benko-ct Nov 25, 2024
830e497
Revert "Move sphere-json-derivation-3 to sphere-json-core (just the m…
benko-ct Dec 5, 2024
ab83fdd
[ENE-49] util-3 and json-3 pure Scala 3 modules
programaker Dec 5, 2024
964f12e
[ENE-49] Remove fmpp from json-3. Remove json-derivation-3 module
programaker Dec 6, 2024
d35e499
Merge pull request #632 from commercetools/scala-3__json-3
benko-ct Dec 9, 2024
43438a2
Add mongoTypeSwitch
benko-ct Jan 30, 2025
f9f0ef5
Add mongoTypeSwitch
benko-ct Jan 30, 2025
e2cf5eb
Fix JSON derivation (moving the actual derivation to FromJSON and ToJ…
benko-ct Feb 21, 2025
e0a00e6
Merge branch 'main' into scala-3
benko-ct Feb 21, 2025
868050b
merge compiler option switches
benko-ct Feb 21, 2025
c6bf620
Remove util-3 as util is compatible with both scala 2 and 3
benko-ct Feb 21, 2025
65573fa
Formatting
benko-ct Feb 21, 2025
d2bd471
Trying to make the pipeline work
benko-ct Feb 21, 2025
214003f
Trying to make the pipeline work
benko-ct Feb 21, 2025
1b46be7
Trying to make the pipeline work
benko-ct Feb 21, 2025
d290d98
Trying to make the pipeline work
benko-ct Feb 21, 2025
f1ff371
Trying to make the pipeline work
benko-ct Feb 21, 2025
eed2c79
Trying to make the pipeline work
benko-ct Feb 21, 2025
c553fc9
Trying to make the pipeline work
benko-ct Feb 21, 2025
40ab176
Trying to make the pipeline work
benko-ct Feb 21, 2025
d1f850d
Remove useless code
benko-ct Feb 21, 2025
7e7c87a
Trying different cross compilation settings
benko-ct Feb 21, 2025
c43a36e
Fix some package names/imports
benko-ct Feb 21, 2025
ac35627
Trying different cross compilation settings
benko-ct Feb 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Fix JSON derivation (moving the actual derivation to FromJSON and ToJ…
…SON)
benko-ct committed Feb 21, 2025
commit e2cf5eb61850f64cfc3bb9cf97134e493b7fd076
129 changes: 113 additions & 16 deletions json/json-3/src/main/scala/io/sphere/json/FromJSON.scala
Original file line number Diff line number Diff line change
@@ -3,27 +3,29 @@ 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
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

18 changes: 11 additions & 7 deletions json/json-3/src/main/scala/io/sphere/json/JSON.scala
Original file line number Diff line number Diff line change
@@ -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)
105 changes: 86 additions & 19 deletions json/json-3/src/main/scala/io/sphere/json/ToJSON.scala
Original file line number Diff line number Diff line change
@@ -1,50 +1,117 @@
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
}

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
*/
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]] =
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[@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)) ::
Original file line number Diff line number Diff line change
@@ -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) {
250 changes: 124 additions & 126 deletions json/json-3/src/main/scala/io/sphere/json/generic/Derivation.scala
Original file line number Diff line number Diff line change
@@ -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]
// }
// }
//}
Original file line number Diff line number Diff line change
@@ -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))
}

}
Original file line number Diff line number Diff line change
@@ -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
35 changes: 15 additions & 20 deletions json/json-3/src/test/scala/io/sphere/json/JSONSpec.scala
Original file line number Diff line number Diff line change
@@ -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)]()
// }
//}
87 changes: 0 additions & 87 deletions json/json-3/src/test/scala/io/sphere/json/TypesSwitchSpec.scala

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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]
}
}
Original file line number Diff line number Diff line change
@@ -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]
}
}
Original file line number Diff line number Diff line change
@@ -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 {
Original file line number Diff line number Diff line change
@@ -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)

}

}
}