Home > Software engineering >  How to validate field path in compile time in Scala 2?
How to validate field path in compile time in Scala 2?

Time:09-07

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)

https://github.com/milessabin/shapeless/wiki/Feature-overview:-shapeless-2.0.0#boilerplate-free-lenses-for-arbitrary-case-classes

// 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 classes, 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 classes 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.

  • Related