Skip to content

Instantly share code, notes, and snippets.

@kcleereman
Last active January 4, 2016 00:39
Show Gist options
  • Save kcleereman/8542753 to your computer and use it in GitHub Desktop.
Save kcleereman/8542753 to your computer and use it in GitHub Desktop.
copyMap macro
package com.sport195.api
import play.api.libs.json._
import scala.language.experimental.macros
object ImplicitConversions {
import java.sql.Timestamp
import java.sql.Date
implicit val x = scala.language.implicitConversions
implicit val readTimestamp: Reads[Timestamp] = (__ \ "time").read[String].map(str => new Timestamp(Date.valueOf(str).getTime()))
implicit def jsonToBool(json: JsValue): Boolean = json.as[Boolean]
implicit def jsonToBoolOpt(json: JsValue): Option[Boolean] = json.asOpt[Boolean]
implicit def jsonToInt(json: JsValue): Int = json.as[Int]
implicit def jsonToIntOpt(json: JsValue): Option[Int] = json.asOpt[Int]
implicit def jsonToStr(json: JsValue): String = json.as[String]
implicit def jsonToStrOpt(json: JsValue): Option[String] = json.asOpt[String]
implicit def jsonToTimestamp(json: JsValue): Timestamp = json.as[Timestamp]
implicit def jsonToTimestampOpt(json: JsValue): Option[Timestamp] = json.asOpt[Timestamp]
}
object Macros {
def copyMap[T](blacklist: String*) = macro copyMapImpl[T]
def copyMapImpl[T: c.WeakTypeTag](c: scala.reflect.macros.Context)(blacklist: c.Expr[String]*): c.Expr[Map[String, (JsValue) => scala.util.Try[(T) => T]]] = {
import c.universe._
val blacklistSet: Set[String] = Set(blacklist.map(e => e.tree match {
case Literal(Constant(str: String)) => str
}): _*)
var blacklistSetCover = Set(blacklistSet.toSeq: _*)
val mappableTypes = Set(typeOf[Option[_]].typeSymbol, typeOf[scala.util.Try[_]].typeSymbol, typeOf[SResult[_]].typeSymbol)
val iterableType = typeOf[Iterable[_]].typeSymbol
val mapType = typeOf[Map[_, _]].typeSymbol
def isIterable(tpe: Type): Boolean = tpe.baseClasses.contains(iterableType) && !isMap(tpe)
def isMap(tpe: Type): Boolean = tpe.baseClasses.contains(mapType)
def isCaseClass(tpe: Type): Boolean = tpe.typeSymbol.isClass && tpe.typeSymbol.asClass.isCaseClass
def isCaseClassMappable(tpe: Type): Boolean = mappableTypes.contains(tpe.typeSymbol) && isCaseClass(tpe.asInstanceOf[TypeRef].args.head)
def rec(tpe: Type): c.Expr[Map[String, (JsValue) => scala.util.Try[(T) => T]]] = {
val typeName = tpe.typeSymbol.name.decoded
val fields = {
val temp = tpe.declarations.collectFirst {
case m: MethodSymbol if m.isPrimaryConstructor => m
}.get.paramss.head
blacklistSetCover = blacklistSetCover -- temp.map(field => typeName + "." + field.name.decoded)
temp.filterNot(field => blacklistSet.contains(typeName + "." + field.name.decoded))
}
val base = fields.filterNot(f => isCaseClass(f.typeSignature) || isCaseClassMappable(f.typeSignature) || isIterable(f.typeSignature) || isMap(f.typeSignature))
val baseIterable = fields.filter(f => isIterable(f.typeSignature))
val baseMaps = fields.filter(f => isMap(f.typeSignature))
val recursive = fields.filter(f => isCaseClass(f.typeSignature))
val recursiveMappable = fields.filter(f => isCaseClassMappable(f.typeSignature))
val baseMethods = base.map {
field => {
val fieldName = field.name
val fieldNameDecoded = fieldName.decoded
val fieldType = field.typeSignature
val fieldTypeName = fieldType.toString
q"""{
import com.sport195.api.ImplicitConversions._
$fieldNameDecoded -> {
(json: JsValue) => scala.util.Try {
val x: $fieldType = json
(t: $tpe) => t.copy($fieldName = x)
}.recoverWith {
case e: Exception => scala.util.Failure(new IllegalArgumentException("Failed to parse " + Json.stringify(json) + " as " + $typeName + "." + $fieldNameDecoded + ": " + $fieldTypeName))
}
}}"""
}}
val baseIterableMethods = baseIterable.map {
field => {
val fieldName = field.name
val fieldNameDecoded = fieldName.decoded
val innerType = field.typeSignature.asInstanceOf[TypeRef].args.head
val innerTypeName = innerType.toString
q"""{
import com.sport195.api.ImplicitConversions._
Seq(
$fieldNameDecoded -> {
(json: JsValue) => scala.util.Try {
val x: Seq[$innerType] = json
(t: $tpe) => t.copy($fieldName = t.$fieldName.genericBuilder[$innerType].++=(x).result)
}.recoverWith {
case e: Exception => scala.util.Failure(new IllegalArgumentException("Failed to parse " + Json.stringify(json) + " as " + $typeName + "." + $fieldNameDecoded + ": Seq[" + $innerTypeName + "]"))
}
}, ($fieldNameDecoded + "++") -> {
(json: JsValue) => scala.util.Try {
val x: Seq[$innerType] = json
(t: $tpe) => t.copy($fieldName = ((t.$fieldName) ++ x))
}.recoverWith {
case e: Exception => scala.util.Failure(new IllegalArgumentException("Failed to parse " + Json.stringify(json) + " as " + $typeName + "." + $fieldNameDecoded + ": Seq[" + $innerTypeName + "]"))
}
}, ($fieldNameDecoded + "--") -> {
(json: JsValue) => scala.util.Try {
val x: Seq[$innerType] = json
(t: $tpe) => t.copy($fieldName = (t.$fieldName).filterNot(f => x.contains(f)))
}.recoverWith {
case e: Exception => scala.util.Failure(new IllegalArgumentException("Failed to parse " + Json.stringify(json) + " as " + $typeName + "." + $fieldNameDecoded + ": Seq[" + $innerTypeName + "]"))
}
})}"""
}}
val baseMapMethods = baseMaps.map {
field => {
val fieldName = field.name
val fieldNameDecoded = fieldName.decoded
val keyType = field.typeSignature.asInstanceOf[TypeRef].args.head
val valType = field.typeSignature.asInstanceOf[TypeRef].args.last
val keyTypeName = keyType.toString
val valTypeName = valType.toString
q"""{
import com.sport195.api.ImplicitConversions._
Seq(
$fieldNameDecoded -> {
(json: JsValue) => scala.util.Try {
val x: Map[$keyType, $valType] = json
(t: $tpe) => t.copy($fieldName = (t.$fieldName.empty ++ x))
}.recoverWith {
case e: Exception => scala.util.Failure(new IllegalArgumentException("Failed to parse " + Json.stringify(json) + " as " + $typeName + "." + $fieldNameDecoded + ": Map[" + $keyTypeName + ", " + $valTypeName + "]"))
}
}, ($fieldNameDecoded + "++") -> {
(json: JsValue) => scala.util.Try {
val x: Map[$keyType, $valType] = json
(t: $tpe) => t.copy($fieldName = ((t.$fieldName) ++ x))
}.recoverWith {
case e: Exception => scala.util.Failure(new IllegalArgumentException("Failed to parse " + Json.stringify(json) + " as " + $typeName + "." + $fieldNameDecoded + ": Map[" + $keyTypeName + ", " + $valTypeName + "]"))
}
}, ($fieldNameDecoded + "--") -> {
(json: JsValue) => scala.util.Try {
val x: Seq[$keyType] = json
(t: $tpe) => t.copy($fieldName = ((t.$fieldName) -- x))
}.recoverWith {
case e: Exception => scala.util.Failure(new IllegalArgumentException("Failed to parse " + Json.stringify(json) + " as " + $typeName + "." + $fieldNameDecoded + ": Seq[" + $keyTypeName + "]"))
}
})}"""
}}
val recursiveMethods = recursive.map {
field => {
val fieldName = field.name
val fieldNameDecoded = fieldName.decoded
val map = rec(field.typeSignature)
q"""{
val innerMap = $map
innerMap.toSeq.map(tuple => ($fieldNameDecoded + "." + tuple._1) -> {
(json: JsValue) => {
val innerUpdate = tuple._2(json)
innerUpdate.map(update => (outer: $tpe) => outer.copy($fieldName = update(outer.$fieldName)))
}})}"""
}}
val recursiveMappableMethods = recursiveMappable.map {
field => {
val fieldName = field.name
val fieldNameDecoded = fieldName.decoded
val map = rec(field.typeSignature.asInstanceOf[TypeRef].args.head)
q"""{
val innerMap = $map
innerMap.toSeq.map(tuple => ($fieldNameDecoded + "." + tuple._1) -> {
(json: JsValue) => {
val innerUpdate = tuple._2(json)
innerUpdate.map(update => (outer: $tpe) => outer.copy($fieldName = {outer.$fieldName}.map(inner => update(inner))))
}})}"""
}}
c.Expr[Map[String, (JsValue) => scala.util.Try[(T) => T]]] {
q"""{ Map((List(..$recursiveMethods).flatten ++ List(..$recursiveMappableMethods).flatten ++ List(..$baseIterableMethods).flatten ++ List(..$baseMapMethods).flatten ++ List(..$baseMethods)):_*) }"""
}
}
val result = rec(weakTypeOf[T])
if(!blacklistSetCover.isEmpty) c.abort(c.enclosingPosition, "Failed to apply entire blacklist: " + blacklistSetCover.mkString("; "))
result
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment