Skip to content

Commit

Permalink
Adding support for spray-json in Scala 3.
Browse files Browse the repository at this point in the history
  • Loading branch information
luksow committed Oct 17, 2024
1 parent 8496f57 commit bc9ae4c
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 5 deletions.
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

0 comments on commit bc9ae4c

Please sign in to comment.