From bc9ae4cd91382bd0f197f8f13b182043047f8a48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Sowa?= Date: Thu, 17 Oct 2024 14:04:14 +0200 Subject: [PATCH] Adding support for spray-json in Scala 3. --- build.sbt | 1 - ...> CirceFormatCapitalizeVariantTests.scala} | 2 +- .../namingconventions/SnakifyVariant.scala | 4 ++ project/plugins.sbt | 2 +- .../kebs/sprayjson/KebsSprayJson.scala | 0 .../sprayjson/macros/KebsSprayMacros.scala | 4 +- .../kebs/sprayjson/KebsSprayJson.scala | 48 +++++++++++++ .../sprayjson/macros/KebsSprayMacros.scala | 71 +++++++++++++++++++ .../kebs/sprayjson/model/package.scala | 2 +- 9 files changed, 129 insertions(+), 5 deletions(-) rename circe/src/test/scala/pl/iterators/kebs/circe/formats/{CirceFormatCapitalizedVariantTests.scala => CirceFormatCapitalizeVariantTests.scala} (98%) rename core/src/main/{scala-2 => scala}/pl/iterators/kebs/core/macros/namingconventions/SnakifyVariant.scala (86%) rename spray-json/src/main/{scala => scala-2}/pl/iterators/kebs/sprayjson/KebsSprayJson.scala (100%) rename spray-json/src/main/{scala => scala-2}/pl/iterators/kebs/sprayjson/macros/KebsSprayMacros.scala (95%) create mode 100644 spray-json/src/main/scala-3/pl/iterators/kebs/sprayjson/KebsSprayJson.scala create mode 100644 spray-json/src/main/scala-3/pl/iterators/kebs/sprayjson/macros/KebsSprayMacros.scala diff --git a/build.sbt b/build.sbt index 502a6268..d536765d 100644 --- a/build.sbt +++ b/build.sbt @@ -337,7 +337,6 @@ lazy val sprayJsonSupport = project .dependsOn(enumeratumSupport.jvm, instances.jvm % "test -> test") .settings(sprayJsonSettings *) .settings(publishSettings *) - .settings(disableScala(List("3"))) .settings( name := "spray-json", description := "Automatic generation of Spray json formats for case-classes", diff --git a/circe/src/test/scala/pl/iterators/kebs/circe/formats/CirceFormatCapitalizedVariantTests.scala b/circe/src/test/scala/pl/iterators/kebs/circe/formats/CirceFormatCapitalizeVariantTests.scala similarity index 98% rename from circe/src/test/scala/pl/iterators/kebs/circe/formats/CirceFormatCapitalizedVariantTests.scala rename to circe/src/test/scala/pl/iterators/kebs/circe/formats/CirceFormatCapitalizeVariantTests.scala index 37841f65..ac671fa7 100644 --- a/circe/src/test/scala/pl/iterators/kebs/circe/formats/CirceFormatCapitalizedVariantTests.scala +++ b/circe/src/test/scala/pl/iterators/kebs/circe/formats/CirceFormatCapitalizeVariantTests.scala @@ -7,7 +7,7 @@ import pl.iterators.kebs.circe.KebsCirceCapitalized import pl.iterators.kebs.circe.model._ import pl.iterators.kebs.core.macros.CaseClass1ToValueClass -class CirceFormatCapitalizedVariantTests extends AnyFunSuite with Matchers { +class CirceFormatCapitalizeVariantTests extends AnyFunSuite with Matchers { object KebsProtocol extends KebsCirceCapitalized with CaseClass1ToValueClass import KebsProtocol._ diff --git a/core/src/main/scala-2/pl/iterators/kebs/core/macros/namingconventions/SnakifyVariant.scala b/core/src/main/scala/pl/iterators/kebs/core/macros/namingconventions/SnakifyVariant.scala similarity index 86% rename from core/src/main/scala-2/pl/iterators/kebs/core/macros/namingconventions/SnakifyVariant.scala rename to core/src/main/scala/pl/iterators/kebs/core/macros/namingconventions/SnakifyVariant.scala index 5c2d0a70..bcef6103 100644 --- a/core/src/main/scala-2/pl/iterators/kebs/core/macros/namingconventions/SnakifyVariant.scala +++ b/core/src/main/scala/pl/iterators/kebs/core/macros/namingconventions/SnakifyVariant.scala @@ -16,3 +16,7 @@ object SnakifyVariant { } } } + +object CapitalizeVariant { + def capitalize(word: String): String = word.capitalize +} diff --git a/project/plugins.sbt b/project/plugins.sbt index a5a4a1a7..0ba63a4c 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -6,5 +6,5 @@ addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.6.1") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.5") diff --git a/spray-json/src/main/scala/pl/iterators/kebs/sprayjson/KebsSprayJson.scala b/spray-json/src/main/scala-2/pl/iterators/kebs/sprayjson/KebsSprayJson.scala similarity index 100% rename from spray-json/src/main/scala/pl/iterators/kebs/sprayjson/KebsSprayJson.scala rename to spray-json/src/main/scala-2/pl/iterators/kebs/sprayjson/KebsSprayJson.scala diff --git a/spray-json/src/main/scala/pl/iterators/kebs/sprayjson/macros/KebsSprayMacros.scala b/spray-json/src/main/scala-2/pl/iterators/kebs/sprayjson/macros/KebsSprayMacros.scala similarity index 95% rename from spray-json/src/main/scala/pl/iterators/kebs/sprayjson/macros/KebsSprayMacros.scala rename to spray-json/src/main/scala-2/pl/iterators/kebs/sprayjson/macros/KebsSprayMacros.scala index 00803e02..ae7671ed 100644 --- a/spray-json/src/main/scala/pl/iterators/kebs/sprayjson/macros/KebsSprayMacros.scala +++ b/spray-json/src/main/scala-2/pl/iterators/kebs/sprayjson/macros/KebsSprayMacros.scala @@ -1,6 +1,7 @@ package pl.iterators.kebs.sprayjson.macros import pl.iterators.kebs.core.macros.MacroUtils +import pl.iterators.kebs.core.macros.namingconventions.CapitalizeVariant.capitalize import spray.json.{JsonFormat, JsonReader, JsonWriter, NullOptions, RootJsonFormat} import scala.reflect.macros._ @@ -101,6 +102,7 @@ object KebsSprayMacros { class SnakifyVariant(context: whitebox.Context) extends KebsSprayMacros(context) { import pl.iterators.kebs.core.macros.namingconventions.SnakifyVariant.snakify + import pl.iterators.kebs.core.macros.namingconventions.CapitalizeVariant.capitalize import c.universe._ override protected def extractJsonFieldNames(fields: List[MethodSymbol]) = super.extractJsonFieldNames(fields).map(snakify) @@ -109,7 +111,7 @@ object KebsSprayMacros { class CapitalizedCamelCase(context: whitebox.Context) extends KebsSprayMacros(context) { import c.universe._ - override protected def extractJsonFieldNames(fields: List[MethodSymbol]) = super.extractJsonFieldNames(fields).map(_.capitalize) + override protected def extractJsonFieldNames(fields: List[MethodSymbol]) = super.extractJsonFieldNames(fields).map(capitalize) } } diff --git a/spray-json/src/main/scala-3/pl/iterators/kebs/sprayjson/KebsSprayJson.scala b/spray-json/src/main/scala-3/pl/iterators/kebs/sprayjson/KebsSprayJson.scala new file mode 100644 index 00000000..f7a95561 --- /dev/null +++ b/spray-json/src/main/scala-3/pl/iterators/kebs/sprayjson/KebsSprayJson.scala @@ -0,0 +1,48 @@ +package pl.iterators.kebs.sprayjson + +import pl.iterators.kebs.core.instances.InstanceConverter +import pl.iterators.kebs.core.macros.ValueClassLike +import spray.json.* +import scala.deriving._ +import scala.compiletime._ + +case class FieldNamingStrategy(transform: String => String) + +trait KebsSprayJson extends LowPriorityKebsSprayJson { self: DefaultJsonProtocol => + implicit def jsonFlatFormat[T, A](implicit rep: ValueClassLike[T, A], baseJsonFormat: JsonFormat[A]): JsonFormat[T] = { + val reader: JsValue => T = json => rep.apply(baseJsonFormat.read(json)) + val writer: T => JsValue = obj => baseJsonFormat.write(rep.unapply(obj)) + jsonFormat[T](reader, writer) + } + implicit def jsonConversionFormat2[T, A](implicit rep: InstanceConverter[T, A], baseJsonFormat: JsonFormat[A]): JsonFormat[T] = { + val reader: JsValue => T = json => rep.decode(baseJsonFormat.read(json)) + val writer: T => JsValue = obj => baseJsonFormat.write(rep.encode(obj)) + jsonFormat[T](reader, writer) + } + + trait KebsSprayJsonSnakified extends KebsSprayJson { self: DefaultJsonProtocol => + import pl.iterators.kebs.core.macros.namingconventions.SnakifyVariant + override implicit def namingStrategy: FieldNamingStrategy = FieldNamingStrategy(SnakifyVariant.snakify) + } + trait KebsSprayJsonCapitalized extends KebsSprayJson { self: DefaultJsonProtocol => + import pl.iterators.kebs.core.macros.namingconventions.CapitalizeVariant + override implicit def namingStrategy: FieldNamingStrategy = FieldNamingStrategy(CapitalizeVariant.capitalize) + } +} + +trait LowPriorityKebsSprayJson { + import macros.KebsSprayMacros + implicit def namingStrategy: FieldNamingStrategy = FieldNamingStrategy(identity) + + def nullOptions: Boolean = this match { + case _: NullOptions => true + case _ => false + } + + inline implicit def jsonFormatN[T <: Product](using m: Mirror.Of[T]): RootJsonFormat[T] = { + KebsSprayMacros.materializeRootFormat[T](nullOptions) + } + + inline final def jsonFormatRec[T <: Product](using m: Mirror.Of[T]): RootJsonFormat[T] = + KebsSprayMacros.materializeRootFormat[T](nullOptions) +} diff --git a/spray-json/src/main/scala-3/pl/iterators/kebs/sprayjson/macros/KebsSprayMacros.scala b/spray-json/src/main/scala-3/pl/iterators/kebs/sprayjson/macros/KebsSprayMacros.scala new file mode 100644 index 00000000..40cc7a56 --- /dev/null +++ b/spray-json/src/main/scala-3/pl/iterators/kebs/sprayjson/macros/KebsSprayMacros.scala @@ -0,0 +1,71 @@ +package pl.iterators.kebs.sprayjson.macros + +import scala.deriving._ +import scala.compiletime._ +import spray.json.* +import pl.iterators.kebs.sprayjson.FieldNamingStrategy + +// this is largely inspired by https://github.com/paoloboni/spray-json-derived-codecs +object KebsSprayMacros { + inline private def label[A]: String = constValue[A].asInstanceOf[String] + + inline def summonAllFormats[A <: Tuple]: List[JsonFormat[_]] = { + inline erasedValue[A] match + case _: EmptyTuple => Nil + case _: (t *: ts) => summonInline[JsonFormat[t]] :: summonAllFormats[ts] + } + + inline def summonAllLabels[A <: Tuple]: List[String] = { + inline erasedValue[A] match { + case _: EmptyTuple => Nil + case _: (t *: ts) => + label[t] :: summonAllLabels[ts] + } + } + + inline def writeElems[T](formats: List[JsonFormat[_]], namingStrategy: FieldNamingStrategy, nullOptions: Boolean)(obj: T): JsValue = { + val pElem = obj.asInstanceOf[Product] + (pElem.productElementNames.toList + .zip(pElem.productIterator.toList) + .zip(formats)) + .map { case ((label, elem), format) => + elem match { + case None if !nullOptions => + JsObject.empty + case e => + JsObject(namingStrategy.transform(label) -> format.asInstanceOf[JsonFormat[Any]].write(e)) + } + } + .foldLeft(JsObject.empty) { case (obj, encoded) => + JsObject(obj.fields ++ encoded.fields) + } + } + + inline def readElems[T]( + p: Mirror.ProductOf[T] + )(labels: List[String], formats: List[JsonFormat[_]], namingStrategy: FieldNamingStrategy)(json: JsValue): T = { + val decodedElems = (labels.map(namingStrategy.transform).zip(formats)).map { case (label, format) => + format.read(json.asJsObject.fields.getOrElse(label, JsNull)) + } + p.fromProduct(Tuple.fromArray(decodedElems.toArray).asInstanceOf) + } + + inline def materializeRootFormat[T <: Product]( + nullOptions: Boolean + )(using m: Mirror.Of[T], namingStrategy: FieldNamingStrategy): RootJsonFormat[T] = { + lazy val formats = summonAllFormats[m.MirroredElemTypes] + lazy val labels = summonAllLabels[m.MirroredElemLabels] + + val format = new RootJsonFormat[T] { + override def read(json: JsValue): T = inline m match { + case s: Mirror.SumOf[T] => error("Sum types are not supported") + case p: Mirror.ProductOf[T] => readElems(p)(labels, formats, namingStrategy)(json) + } + override def write(obj: T): JsValue = inline m match { + case s: Mirror.SumOf[T] => error("Sum types are not supported") + case p: Mirror.ProductOf[T] => writeElems(formats, namingStrategy, nullOptions)(obj) + } + } + format + } +} diff --git a/spray-json/src/test/scala/pl/iterators/kebs/sprayjson/model/package.scala b/spray-json/src/test/scala/pl/iterators/kebs/sprayjson/model/package.scala index 0b2394a7..29873863 100644 --- a/spray-json/src/test/scala/pl/iterators/kebs/sprayjson/model/package.scala +++ b/spray-json/src/test/scala/pl/iterators/kebs/sprayjson/model/package.scala @@ -2,7 +2,7 @@ package pl.iterators.kebs.sprayjson package object model { - case class F1(f1: String) extends AnyVal + case class F1(f1: String) case class ClassWith23Fields( f1: F1,