Skip to content

Instantly share code, notes, and snippets.

@dominikbucher
Last active August 29, 2015 14:03
Show Gist options
  • Save dominikbucher/8eefd28875506932e265 to your computer and use it in GitHub Desktop.
Save dominikbucher/8eefd28875506932e265 to your computer and use it in GitHub Desktop.
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, valid[User]("properties.validated") would yield true.
*
* This is useful for compile time checking when doing MongoDB queries
* for example. For further details, pitfalls, and why another
* solution is better, visit:
*
* http://dominikbucher.wordpress.com/2014/07/07/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 valid[T](accessor: String) = macro valid_impl[T]
def valid_impl[T: c.WeakTypeTag](c: Context)(accessor: c.Expr[String]): c.Expr[String] = {
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
}
@tailrec
def hasField(accessor: List[String], tpe: c.universe.Type): Boolean = {
accessor match {
case head :: Nil => {
if (getFields(tpe).filter(_.name.decoded == head).isEmpty) false else true
}
case head :: tail => {
if (getFields(tpe).filter(_.name.decoded == head).isEmpty) false else {
hasField(tail, tpe.declaration(newTermName(head)).typeSignature)
}
}
}
}
val accessorRep = accessor.tree match {
case Literal(Constant(str: String)) => str
}
val accessorParts = accessorRep.split("\\.").toList
val validAccessor = hasField(accessorParts, typeOfT)
if (validAccessor) accessor
else {
c.error(c.enclosingPosition, s"Field accessor ${accessorRep} of type ${typeOfT} is invalid.")
accessor
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment