Suppose I have a string with a field path of a Scala case class, e.g.
case class A1(x: Int)
case class A(a1: A1)
val x = "a1.x" // field path of "x" in "A"
I use this field path for runtime reflection. The problem is that these classes A
and A1
may change and then the field path becomes invalid.
case class A1(x1: Int)
case class A(a1: A1)
val x = "a1.x" // invalid path in "A"
Now I want to validate the field path in compile time like this:
case class A1(x1: Int)
case class A(a1: A1)
val x = FieldPath[A]("a1.x") // compiler error
What is the best way to do it with Scala 2 ? I guess it's doable using Scala 2 macros but I don't know how to do that.
CodePudding user response:
What you want is actually close to lenses
https://www.optics.dev/Monocle/docs/focus
// libraryDependencies = "dev.optics" %% "monocle-core" % "3.1.0"
// libraryDependencies = "dev.optics" %% "monocle-macro" % "3.1.0"
import monocle.syntax.all._
(??? : A).focus(_.a1.x1)
// libraryDependencies = "com.chuusai" %% "shapeless" % "2.3.9"
import shapeless.lens
lens[A] >> 'a1 >> 'x1
Maybe lenses would be enough for you. If you really want to validate strings like "a1.x"
you can write a macro reusing Monocle or Shapeless functionality
import scala.language.experimental.macros
import scala.reflect.macros.whitebox
def FieldPath[A](s: String): Unit = macro FieldPathImpl[A]
def FieldPathImpl[A: c.WeakTypeTag](c: whitebox.Context)(s: c.Tree): c.Tree = {
import c.universe._
val s1 = c.eval(c.Expr[String](s))
c.typecheck(c.parse(s"{ import _root_.monocle.syntax.all._; (??? : ${weakTypeOf[A].typeSymbol.fullName}).focus(_.$s1)}"))
//c.typecheck(c.parse(s"_root_.shapeless.lens[${weakTypeOf[A].typeSymbol.fullName}] >> '${s1.replace(".", " >> '")}"))
q"()"
}
CodePudding user response:
I'm not 100 % sure I understand your requirements, but before complicating with macros, maybe something simpler like this could work for you:
def isPathValid[T <: Product](s: String)(obj: T) = {
val arr = s.split("\\.", 2)
arr.length == 2 &&
obj.getClass.getSimpleName.equalsIgnoreCase(arr(0)) &&
obj.productElementNames.contains(arr(1))
}
case class A1(x1: Int)
val a1 = A1(1)
println(isPathValid[A1]("a1.x")(a1)) // false
println(isPathValid[A1]("a1.x1")(a1)) // true
Since Scala 2.13, case class
es, which already inherit the Product
trait, provide the productElementNames
method which results in an iterator over their field's names. You can tailor the String
camel case comparison variable names to your particular needs, if you want to ignore them or not.
I'm aware this is not exactly a compile-time solution as you wanted, but even if the case class
es change, as longs as you can include a check with isPathValid
and provide an instance of the type you want to validate, this should work.