Skip to content

Instantly share code, notes, and snippets.

@dominikbucher
Last active August 29, 2015 14:04
Show Gist options
  • Save dominikbucher/6d710527814d0ac0422e to your computer and use it in GitHub Desktop.
Save dominikbucher/6d710527814d0ac0422e to your computer and use it in GitHub Desktop.
Improved Scala Macro for Safe Field Access in MongoDB
/**
* 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