in scalding-core/src/main/scala/com/twitter/scalding/macros/impl/CaseClassBasedSetterImpl.scala [27:124]
def apply[T](c: Context)(container: c.TermName, allowUnknownTypes: Boolean, fsetter: CaseClassFieldSetter)(
implicit T: c.WeakTypeTag[T]
): (Int, c.Tree) = {
import c.universe._
sealed trait SetterBuilder {
def columns: Int
/**
* This Tree assumes that "val $value = ..." has been set
*/
def setTree(value: Tree, offset: Int): Tree
}
final case class PrimitiveSetter(tpe: Type) extends SetterBuilder {
def columns = 1
def setTree(value: Tree, offset: Int) = fsetter.from(c)(tpe, offset, container, value) match {
case Success(tree) => tree
case Failure(e) =>
c.abort(c.enclosingPosition, s"Case class $T is supported. Error on $tpe, ${e.getMessage}")
}
}
case object DefaultSetter extends SetterBuilder {
def columns = 1
def setTree(value: Tree, offset: Int) = fsetter.default(c)(offset, container, value)
}
final case class OptionSetter(inner: SetterBuilder) extends SetterBuilder {
def columns = inner.columns
def setTree(value: Tree, offset: Int) = {
val someVal = newTermName(c.fresh("someVal"))
val someValTree = q"$someVal"
q"""if($value.isDefined) {
val $someVal = $value.get
${inner.setTree(someValTree, offset)}
} else {
${fsetter.absent(c)(offset, container)}
}"""
}
}
final case class CaseClassSetter(members: Vector[(Tree => Tree, SetterBuilder)]) extends SetterBuilder {
val columns = members.map(_._2.columns).sum
def setTree(value: Tree, offset: Int) = {
val setters = members
.scanLeft((offset, Option.empty[Tree])) { case ((off, _), (access, sb)) =>
val cca = newTermName(c.fresh("access"))
val ccaT = q"$cca"
(off + sb.columns, Some(q"val $cca = ${access(value)}; ${sb.setTree(ccaT, off)}"))
}
.collect { case (_, Some(tree)) => tree }
q"""..$setters"""
}
}
@annotation.tailrec
def normalized(tpe: Type): Type = {
val norm = tpe.normalize
if (!(norm =:= tpe))
normalized(norm)
else
tpe
}
def matchField(outerType: Type): SetterBuilder = {
// we do this just to see if the setter matches.
val dummyIdx = 0
val dummyTree = q"t"
outerType match {
case tpe if fsetter.from(c)(tpe, dummyIdx, container, dummyTree).isSuccess =>
PrimitiveSetter(tpe)
case tpe if tpe.erasure =:= typeOf[Option[Any]] =>
val innerType = tpe.asInstanceOf[TypeRefApi].args.head
OptionSetter(matchField(innerType))
case tpe if tpe.typeSymbol.isClass && tpe.typeSymbol.asClass.isCaseClass =>
CaseClassSetter(expandMethod(normalized(tpe)).map { case (fn, tpe) =>
(fn, matchField(tpe))
})
case tpe if allowUnknownTypes =>
DefaultSetter
case _ =>
c.abort(c.enclosingPosition, s"Case class ${T.tpe} is not supported at type: $outerType")
}
}
def expandMethod(outerTpe: Type): Vector[(Tree => Tree, Type)] =
outerTpe.declarations
.collect { case m: MethodSymbol if m.isCaseAccessor => m }
.map { accessorMethod =>
val fieldType =
normalized(accessorMethod.returnType.asSeenFrom(outerTpe, outerTpe.typeSymbol.asClass))
({ pTree: Tree => q"""$pTree.$accessorMethod""" }, fieldType)
}
.toVector
// in TupleSetterImpl, the outer-most input val is called t, so we pass that in here:
val sb = matchField(normalized(T.tpe))
if (sb.columns == 0)
c.abort(c.enclosingPosition, "Didn't consume any elements in the tuple, possibly empty case class?")
(sb.columns, sb.setTree(q"t", 0))
}