Last active
January 4, 2016 00:39
-
-
Save kcleereman/8542753 to your computer and use it in GitHub Desktop.
copyMap macro
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
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