scalamacrosscala-quasiquotes

Scala: pattern inside a q interpolator


I am looking at a piece of Scala macro which provides an implicit implementation of a class. This class converts a map of field values to a case class. The macro can be found here and this is the explanation behind it.

Currently the implementation ignores redundant field provided in the input map. I want to add a method similar to fromMap which would throw an exception if the input map has redundant entries but I am not sure if I understand it well enough.

My understanding is that toMapParams and fromMapParams are expressions that take the inputs and apply either Map or the Companion object's Apply method on them.

As such, fromMapParams should be modified to output the redundant values as a list of string in an exception of the form:

case class CaseClassRedundantFieldException[T]
(redundantFields: List[String], cause: Throwable = None.orNull)
(implicit c: ClassTag[T])
extends Exception(s"Conversion between map of data-fields to ${c.runtimeClass.asInstanceOf[T]} failed" 
+ "as redundant fields were provided: $redundantFields",
    cause)

I think I need to simply have something like:

def fromMap(map: Map[String, Any]): $tpe = {
val redundant vals: fields diff $map 
if(vals.size > 0){ CaseClassRedundantFieldException(vals) } //(these two lines don't have the right syntax.)
$companion(..$fromMapParams )

}

How can I do this?


Solution

  • Unfortunately you didn't provide a Minimal, Complete, and Verifiable example of what you have now so I had to go back to what you started with. I think this modified macro does something quite similar to what you want:

    def materializeMappableImpl[T: c.WeakTypeTag](c: Context): c.Expr[Mappable[T]] = {
      import c.universe._
      val tpe = weakTypeOf[T]
      val className = tpe.typeSymbol.name.toString
      val companion = tpe.typeSymbol.companion
    
      val fields = tpe.decls.collectFirst {
        case m: MethodSymbol if m.isPrimaryConstructor ⇒ m
      }.get.paramLists.head
    
      val (toMapParams, fromMapParams, fromMapParamsList) = fields.map { field ⇒
        val name = field.name.toTermName
        val decoded = name.decodedName.toString
        val returnType = tpe.decl(name).typeSignature
    
        (q"$decoded → t.$name", q"map($decoded).asInstanceOf[$returnType]", decoded)
      }.unzip3
    
      c.Expr[Mappable[T]] {
        q"""
        new Mappable[$tpe] {
          private val fieldNames = scala.collection.immutable.Set[String](..$fromMapParamsList)
          def toMap(t: $tpe): Map[String, Any] = Map(..$toMapParams)
          def fromMap(map: Map[String, Any]): $tpe = {
            val redundant = map.keys.filter(k => !fieldNames.contains(k))
            if(!redundant.isEmpty) throw new IllegalArgumentException("Conversion between map of data-fields to " + $className + " failed because there are redundant fields: " + redundant.mkString("'","', ","'"))
            $companion(..$fromMapParams)
          }
        }
      """
      }
    }