Skip to content

Commit

Permalink
Move sphere-json-derivation-3 to sphere-json-core (just the main, not…
Browse files Browse the repository at this point in the history
… the tests for now)
  • Loading branch information
benko-ct committed Jul 20, 2024
1 parent f4311e6 commit f2cbf08
Show file tree
Hide file tree
Showing 5 changed files with 312 additions and 1 deletion.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
147 changes: 147 additions & 0 deletions json/json-core/src/main/scala-3/io/sphere/json/JSON.scala
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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]
}
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit f2cbf08

Please sign in to comment.