Skip to content

Instantly share code, notes, and snippets.

@jodersky
Created August 26, 2017 22:24
Show Gist options
  • Save jodersky/8b5c1dddb92a2bd7d4895e94494b9420 to your computer and use it in GitHub Desktop.
Save jodersky/8b5c1dddb92a2bd7d4895e94494b9420 to your computer and use it in GitHub Desktop.
Spray-JSON formats for product types, automatically provided with the use of macros.
package spray.json
import scala.language.experimental.macros
import scala.reflect.macros.blackbox.Context
trait MacroFormats {
def macroReader[A]: RootJsonReader[A] = macro MacroFormatsBundle.jsonReader[A]
def macroWriter[A]: RootJsonWriter[A] = macro MacroFormatsBundle.jsonWriter[A]
def macroFormat[A]: RootJsonFormat[A] = macro MacroFormatsBundle.jsonFormat[A]
}
object MacroFormats extends MacroFormats
private[json] class MacroFormatsBundle(val c: Context) {
import c.universe._
private def checkProduct[A: c.WeakTypeTag]: Type = {
val product = weakTypeOf[A]
if (!(product <:< weakTypeOf[Product])) {
c.abort(c.enclosingPosition,
s"Cannot generate reader for non-product type $product")
}
product
}
/** Summon an implicit value and return the tree representing its
* invocation, or fail if no implicit is available. */
private def implicitlyOrFail(tpe: Type, message: String): Tree = {
c.typecheck(
q"""{
import ${c.prefix}._
implicitly[$tpe]
}""",
silent = true
) match {
case EmptyTree => c.abort(c.enclosingPosition, message)
case tree => tree
}
}
def jsonReader[A: c.WeakTypeTag]: c.Tree = {
val product = checkProduct[A]
val fields: Map[TermName, Type] = product.decls.collect {
case m: MethodSymbol if m.isCaseAccessor =>
m.name -> m.info.resultType
}.toMap
val readers: Map[TermName, Tree] = fields.map {
case (name, tpe) =>
name -> implicitlyOrFail(
appliedType(typeOf[JsonReader[_]], tpe),
s"No implicit reader available for $product.$name of type $tpe")
}
val mapReaderType = typeOf[JsonReader[Map[String, JsValue]]]
val mapReader = implicitlyOrFail(
mapReaderType,
s"No implicit reader available for ${mapReaderType}."
)
q"""new _root_.spray.json.RootJsonReader[$product]{
override def read(js: spray.json.JsValue): $product = {
val fields = $mapReader.read(js)
new $product(
..${readers.map {
case (name, reader) =>
q"$name = $reader.read(fields(${name.toString}))"
}}
)
}
}"""
}
def jsonWriter[A: c.WeakTypeTag]: c.Tree = {
val product = checkProduct[A]
val fields: Map[TermName, Type] = product.decls.collect {
case m: MethodSymbol if m.isCaseAccessor =>
m.name -> m.info.resultType
}.toMap
val writers: Map[TermName, Tree] = fields.map {
case (name, tpe) =>
name -> implicitlyOrFail(
appliedType(typeOf[JsonWriter[_]], tpe),
s"No implicit writer available for $product.$name of type $tpe")
}
val mapWriterType = typeOf[JsonWriter[Map[String, JsValue]]]
val mapWriter = implicitlyOrFail(
mapWriterType,
s"No implicit writer available for ${mapWriterType}."
)
q"""new _root_.spray.json.RootJsonWriter[$product]{
override def write(cc: $product): _root_.spray.json.JsValue = {
val fields = _root_.scala.collection.immutable.Map(
..${writers.map {
case (name, writer) =>
q"${name.toString} -> $writer.write(cc.$name)"
}}
)
$mapWriter.write(fields)
}
}"""
}
def jsonFormat[A: c.WeakTypeTag]: c.Tree = {
val product = checkProduct[A]
q"""{
val reader = macroReader[$product]
val writer = macroWriter[$product]
new _root_.spray.json.RootJsonFormat[$product]{
override def read(js: spray.json.JsValue) = reader.read(js)
override def write(cc: $product) = writer.write(cc)
}
}"""
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment