From 830e49706e7fee1dbde4a479fef70ee1d0738b22 Mon Sep 17 00:00:00 2001 From: Benko Balog Date: Thu, 5 Dec 2024 17:40:11 +0100 Subject: [PATCH] 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