Skip to content

Commit

Permalink
move mongo-derivation-3 to mongo-3, separating the MongoFormat implem…
Browse files Browse the repository at this point in the history
…entation from mongo-core making it a fully standalone scala3 module
  • Loading branch information
benko-ct committed Nov 22, 2024
1 parent 185d1c7 commit 7b84b3c
Show file tree
Hide file tree
Showing 23 changed files with 298 additions and 269 deletions.
12 changes: 6 additions & 6 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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`)
Expand All @@ -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"))
Expand Down
3 changes: 3 additions & 0 deletions mongo/mongo-3/dependencies.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
libraryDependencies ++= Seq(
"org.mongodb" % "mongodb-driver-core" % "5.1.2" // tracking http://mongodb.github.io/mongo-java-driver/
)
Original file line number Diff line number Diff line change
@@ -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
}
}
8 changes: 8 additions & 0 deletions mongo/mongo-3/src/main/scala/io/sphere/mongo/format.scala
Original file line number Diff line number Diff line change
@@ -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)
152 changes: 152 additions & 0 deletions mongo/mongo-3/src/main/scala/io/sphere/mongo/format/MongoFormat.scala
Original file line number Diff line number Diff line change
@@ -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]
}

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

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

Expand All @@ -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)
Expand All @@ -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)
Expand Down
Loading

0 comments on commit 7b84b3c

Please sign in to comment.