Question

I have a map of case class copy methods constructed using a macro (How to use scala macros to create a function object (to create a Map[String, (T) => T])), using implicit conversions to convert an input to the correct type of the copy method being called (JsValue is a Play Framework json value)

case class Clazz(int: Int, string: String)

implicit def jsonToInt(json: JsValue): Int = json.as[Int]
implicit def jsonToStr(json: JsValue): String = json.as[String]

val copyMap: Map[String, (Clazz, JsValue) => Clazz] = 
  Map("int" -> (c: Clazz, json: JsValue) => c.copy(int = json),
      "string" -> (c: Clazz, json: JsValue) => c.copy(string = json))

I would like to partially apply an input to a function retrieved from copyMap, e.g.

val fun: (Clazz, JsValue) => Clazz = copyMap.get("int").get
val newFun: (Clazz) => Clazz = fun(_: Clazz, JsString("non_int_input"))

In this case the function will throw an exception when trying to convert JsString("non_int_input") to an Int when fully applying the function, but I'd like to get this error when partially applying the JsValue input - fully applying the function involves retrieving a Clazz instance from a database, and so I'd rather preempt this if the string input is invalid.


Is there a way that I can trigger / extract any implicit conversion errors in the partially applied function?

Was it helpful?

Solution

I think I may have found a solution

case class Clazz(int: Int, string: String)

implicit def jsonToInt(json: JsValue) = json.as[Int]
implicit def jsonToStr(json: JsValue) = json.as[String]

val copyMap[String, (JsValue) => Try[(Clazz) => Clazz]] = 
  Map(
    "int" -> {
      (json: JsValue) => Try[(Clazz) => Clazz] = 
        Try {
          val i: Int = json
          (c: Clazz) => c.copy(int = i)
        }
      },
    "string" -> {
      (json: JsValue) => Try[(Clazz) => Clazz] = 
        Try {
          val s: String = json
          (c: Clazz) => c.copy(string = s)
        }
      }
    )

newFun will be a Failure when I call

val fun: (JsValue) => Try[(Clazz) => Clazz] = copyMap.get("int").get
val newFun: Try[(Clazz) => Clazz] = fun(JsString("non_int_input"))

The macro code for this is (I ran into an import conflict with a Try in c.universe._, hence the use of scala.util.Try)

import play.api.libs.json._
import scala.language.experimental.macros

object Macros {
  implicit def jsonToInt(json: JsValue): Int = json.as[Int]
  implicit def jsonToIntOpt(json: JsValue): Option[Int] = json.asOpt[Int]
  implicit def jsonToStr(json: JsValue): String = json.as[String]
  implicit def jsonToStrOpt(json: JsValue): Option[String] = json.asOpt[String]

  def copyMap[T]: Map[String, (JsValue) => scala.util.Try[(T) => T]] = macro copyMapImpl[T]

  def copyMapImpl[T: c.WeakTypeTag](c: scala.reflect.macros.Context): 
      c.Expr[Map[String, (JsValue) => scala.util.Try[(T) => T]]] = {

    import c.universe._
    val tpe = weakTypeOf[T]

    val fields = tpe.declarations.collectFirst {
      case m: MethodSymbol if m.isPrimaryConstructor => m
    }.get.paramss.head

    val methods = fields.map {
      field => {
        val name = field.name
        val decoded = name.decoded
        val fieldType = field.typeSignature.typeSymbol
        val fieldTypeName = fieldType.name.decoded
        q"""{$decoded -> {
          (json: JsValue) => scala.util.Try {
           val x: $fieldType = json
           (t: $tpe) => t.copy($name = x)
          }.recoverWith {
            case e: Exception => scala.util.Failure(
              new IllegalArgumentException("Failed to parse " + Json.stringify(json) + " as " + $decoded + ": " + $fieldTypeName)
            )
          }
        }}"""
      }
    }

    c.Expr[Map[String, (JsValue) => scala.util.Try[(T) => T]]] {
      q"Map(..$methods)"
    }
  }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top