Last active
August 29, 2015 14:04
-
-
Save dominikbucher/6d710527814d0ac0422e to your computer and use it in GitHub Desktop.
Improved Scala Macro for Safe Field Access in MongoDB
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
/** | |
* Checks if a field accessor string like "properties.validated" is valid | |
* within nested case classes, e.g.: | |
* | |
* case class User(val properties: UserProperties) | |
* case class UserProperties(val validated: Boolean) | |
* | |
* In the above example, create an accessor object by calling | |
* | |
* val acs = accessors[User] | |
* | |
* on the User companion object. Accessors can now be validated as | |
* | |
* User.acs.properties.validated | |
* | |
* which will yield the string "properties.validated" if the corresponding | |
* field actually exinsts in the model class. | |
* | |
* This is useful for compile time checking when doing MongoDB queries | |
* for example. For further details visit: | |
* | |
* http://dominikbucher.wordpress.com/2014/07/25/improved-scala-macros-for-safe-field-access-in-mongodb/ | |
*/ | |
import scala.reflect.macros.Context | |
import scala.language.experimental.macros | |
import scala.annotation.tailrec | |
object Accessors { | |
def buildAccessors[T] = macro buildAccessors_impl[T] | |
def buildAccessors_impl[T: c.WeakTypeTag](c: Context): c.Expr[Any] = { | |
import c.universe._ | |
val typeOfT = weakTypeOf[T] | |
def getFields(tpe: c.universe.Type) = { | |
tpe.members.collectFirst { | |
case m: MethodSymbol if m.isPrimaryConstructor => m | |
}.get.paramss.head | |
} | |
def buildSubAccessor(prefix: String, tpe: c.universe.Type): c.Expr[Any] = { | |
val types = getFields(tpe).map { field => | |
val fieldName = field.name.decoded | |
val caseAccessors = field.typeSignature.declarations.collect { | |
case m: MethodSymbol if m.isCaseAccessor => m | |
} | |
val genericOpt = field.typeSignature match { | |
case paramType @ TypeRef(_, _, args) => { | |
if (args.length == 1) Some(args.head) else None | |
} | |
case _ => None | |
} | |
val fullAccessor = if (prefix.length == 0) fieldName else s"$prefix.$fieldName" | |
if (caseAccessors.isEmpty && !genericOpt.isDefined) { | |
c.Expr[Any] { q"val ${newTermName(fieldName)} = $fullAccessor" } | |
} else if (genericOpt.isDefined) { | |
val sub = buildSubAccessor(fullAccessor, genericOpt.get) | |
c.Expr[Any] { q"val ${newTermName(fieldName)} = $sub" } | |
} else { | |
val sub = buildSubAccessor(fullAccessor, field.typeSignature) | |
c.Expr[Any] { q"val ${newTermName(fieldName)} = $sub" } | |
} | |
} | |
val anon = newTypeName(c.fresh) | |
// Dummy to make sure variables don't get declared as private. | |
val dmmy = newTermName(c.fresh) | |
c.Expr[Any] { | |
q""" | |
class $anon extends Accessor { | |
override def toString = $prefix | |
..$types | |
} | |
val $dmmy = 0 | |
new $anon | |
""" | |
} | |
} | |
buildSubAccessor("", typeOfT) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment