-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding support for spray-json in Scala 3.
- Loading branch information
Showing
9 changed files
with
129 additions
and
5 deletions.
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
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
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
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.
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
48 changes: 48 additions & 0 deletions
48
spray-json/src/main/scala-3/pl/iterators/kebs/sprayjson/KebsSprayJson.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,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) | ||
} |
71 changes: 71 additions & 0 deletions
71
spray-json/src/main/scala-3/pl/iterators/kebs/sprayjson/macros/KebsSprayMacros.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,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 | ||
} | ||
} |
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