0

I have about 24 Case classes that I need to programatically enhance by changing several common elements prior to serialization in a datastore that doesn't support joins. Since case classes don't have a trait defined for the copy(...) constructor, I have been attempting to use Macros - As a base I've looked at this post documenting a macro and come up with this macro:

When I try to compile, I get the following:

import java.util.UUID

import org.joda.time.DateTime
import scala.language.experimental.macros


trait RecordIdentification {
  val receiverId: Option[String]
  val transmitterId: Option[String]
  val patientId: Option[UUID]
  val streamType: Option[String]
  val sequenceNumber: Option[Long]
  val postId: Option[UUID]
  val postedDateTime: Option[DateTime]
}

object WithRecordIdentification {
  import scala.reflect.macros.Context

  def withId[T, I](entity: T, id: I): T = macro withIdImpl[T, I]

  def withIdImpl[T: c.WeakTypeTag, I: c.WeakTypeTag](c: Context)(
    entity: c.Expr[T], id: c.Expr[I]
  ): c.Expr[T] = {
    import c.universe._

    val tree = reify(entity.splice).tree
    val copy = entity.actualType.member(newTermName("copy"))

    val params = copy match {
      case s: MethodSymbol if (s.paramss.nonEmpty) => s.paramss.head
      case _ => c.abort(c.enclosingPosition, "No eligible copy method!")
    }

    c.Expr[T](Apply(
      Select(tree, copy),
      AssignOrNamedArg(Ident("postId"), reify(id.splice).tree) ::
      AssignOrNamedArg(Ident("patientId"), reify(id.splice).tree) ::
      AssignOrNamedArg(Ident("receiverId"), reify(id.splice).tree) ::
      AssignOrNamedArg(Ident("transmitterId"), reify(id.splice).tree) ::
      AssignOrNamedArg(Ident("sequenceNumber"), reify(id.splice).tree) :: Nil
    ))
  }
}

And I invoke it with something like:

class GenericAnonymizer[A <: RecordIdentification]() extends Schema {
  def anonymize(dataPost: A, header: DaoDataPostHeader): A = WithRecordIdentification.withId(dataPost, header)
}

But I get a compile error:

Error:(44, 71) type mismatch;
 found   : com.dexcom.rt.model.DaoDataPostHeader
 required: Option[String]
    val copied = WithRecordIdentification.withId(sampleGlucoseRecord, header)
Error:(44, 71) type mismatch;
 found   : com.dexcom.rt.model.DaoDataPostHeader
 required: Option[java.util.UUID]
    val copied = WithRecordIdentification.withId(sampleGlucoseRecord, header)
Error:(44, 71) type mismatch;
 found   : com.dexcom.rt.model.DaoDataPostHeader
 required: Option[Long]
    val copied = WithRecordIdentification.withId(sampleGlucoseRecord, header)

I'm not quite sure how to change the macro to support multiple parameters... any sage advice?

Community
  • 1
  • 1
Steven Fines
  • 467
  • 4
  • 14
  • can you explain more on "I need to programatically enhance by changing several common elements prior to serialization" ? supposing you have 2 case classes: `case class Foo(a: String, b: String) case class Bar(x: String, y: Int)` – phantomastray Nov 30 '16 at 18:51
  • So, suppose I have 25 case classes that all have common key elements (see the RecordIdentification case class in the example); all of these classes need to be anonymized which means that I have to replace the elements. For a single case class, it's easiest and makes the most sense to use the copy constructor. To save myself a lot of duplicate code, I'm searching for a way to do this in a more systematic fashion. – Steven Fines Nov 30 '16 at 19:00
  • AFAIK `Macros` are used for compile-time reflection. What you need is runtime reflection. Check my answer. – phantomastray Nov 30 '16 at 20:38
  • I want the compile time for type safety and for efficiency. – Steven Fines Nov 30 '16 at 21:01
  • ok, if that's the case. You can use my approach if you choose to use runtime. :) – phantomastray Nov 30 '16 at 21:10

1 Answers1

1

Assuming you have a set of following case classes, which you wish to anonymize on certain attributes prior to serialization.

case class MyRecordA(var receiverId: String, var y: Int)
case class MyRecordB(var transmitterId: Int, var y: Int)
case class MyRecordC(var patientId: UUID, var y: Int)
case class MyRecordD(var streamType: String, var y: Int)
case class MyRecordE(var sequenceNumber: String, var streamType: String, var y: Int)

You can use scala reflection library to mutate an instance's attributes in runtime. You can implement your custom anonymize/enhancing logic in implicit anonymize method that the Mutator can use to alter a given instance's field selectively if required as per your implementation.

import java.util.UUID
import scala.reflect.runtime.{universe => ru}

implicit def anonymize(field: String /* field name */, value: Any /* use current field value if reqd */): Option[Any] = field match {
    case "receiverId" => Option(value.toString.hashCode)
    case "transmitterId" => Option(22)
    case "patientId" => Option(UUID.randomUUID())
    case _ => None
}

implicit class Mutator[T: ru.TypeTag](i: T)(implicit c: scala.reflect.ClassTag[T], anonymize: (String, Any) => Option[Any]) {

    def mask = {
        val m = ru.runtimeMirror(i.getClass.getClassLoader)
        ru.typeOf[T].members.filter(!_.isMethod).foreach(s => {
            val fVal = m.reflect(i).reflectField(s.asTerm)
            anonymize(s.name.decoded.trim, fVal.get).foreach(fVal.set)
        })

        i
    }
}

Now you can invoke masking on any instance as:

val maskedRecord = MyRecordC(UUID.randomUUID(), 2).mask
phantomastray
  • 449
  • 3
  • 16