Reputation: 42050
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.
Upvotes: 1
Views: 114
Reputation: 2638
I don't know your specific 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 then tailor the comparison of camel case String
variable names to your particular needs.
I'm aware it's 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.
Upvotes: 1
Reputation: 51658
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"()"
}
Upvotes: 2