Michael
Michael

Reputation: 42050

How to validate field path in compile time in Scala 2?

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

Answers (2)

Alin Gabriel Arhip
Alin Gabriel Arhip

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

Upvotes: 1

Dmytro Mitin
Dmytro Mitin

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)

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"()"
}

Upvotes: 2

Related Questions