-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Move sphere-json-derivation-3 to sphere-json-core (just the main, not…
… the tests for now)
- Loading branch information
Showing
5 changed files
with
312 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
147 changes: 147 additions & 0 deletions
147
json/json-core/src/main/scala-3/io/sphere/json/JSON.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
153 changes: 153 additions & 0 deletions
153
json/json-core/src/main/scala-3/io/sphere/json/generic/AnnotationReader.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
} |
11 changes: 11 additions & 0 deletions
11
json/json-core/src/main/scala-3/io/sphere/json/generic/JSONAnnotation.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |