Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for spray-json in Scala 3 #409

Merged
merged 1 commit into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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._

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ object SnakifyVariant {
}
}
}

object CapitalizeVariant {
def capitalize(word: String): String = word.capitalize
}
2 changes: 1 addition & 1 deletion project/plugins.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Original file line number Diff line number Diff line change
@@ -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._
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}

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