Home > Software design >  Iterate through parameters and optional values in a case class checking that only certain fields are
Iterate through parameters and optional values in a case class checking that only certain fields are

Time:12-30

I have a case class like the following except it has far more parameters (all optional):

case class MyClass(i: Option[String], j: Option[Int], n: Option[OtherClass])

I want to loop over the parameters and verify that only i and j are defined, while the rest are None. If the case class changes in the future I want to keep the same validation in place, which is why I am not using pattern matching or explicitly checking.

What is a good a way of doing this in scala 2.12?

CodePudding user response:

You can do this using productIterator which is a list of fields for the case class:

case class MyClass(i: Option[String], j: Option[Int], n: Option[OtherClass])
{
  def verify = {
    val c = this.productIterator.count{
      case o: Option[_] => o.nonEmpty
      case _ => true
    }
    i.nonEmpty && j.nonEmpty && c == 2
  }
}

However this kind of generic code has risks, so I would recommend avoiding a case class with a lot of fields. Consider grouping various fields into their own object and the making the test explicit on the known fields. Updating the test when the number of fields changes is not a great chore.

CodePudding user response:

You could use productIterator as @Tim explains, however this smells kinda bad. If there is a special situation in which only MyClass(Some(_), Some(_), None) is valid, but MyClass(Some(_), Some(_), Some(_)) is not, it appears that these should be two distinct types rather than a "catchall" class with ad-hoc validation tackled on the side.

Consider something like this:

    sealed trait Foo {
      def i: Option[String]
      def j: Option[Int]
      def n: Option[OtherClass] = None
   }

   case class IJ(iVal: String, jVal: Int) extends Foo {
      def i = Some(iVal)
      def j = Some(jVal) 
   }
   
   case class IJN(i: Option[String], j: Option[Int], nVal: OtherClass) extends Foo {
      override def n = Some(nVal)
   }

This way, you can simply check the type of the object you are dealing with to distinguish between the two situations:

     foo match { 
       case x: IJ => "i and j are defined, but n is not"
       case x: IJN => "N is defined, but i and j might not be"
    }

CodePudding user response:

I figured out you can do this through reflection, although it may not be recommended see note below:

val allowableFieldNames = Set("i", "j")   
val fields = universe.typeOf[MyClass].decls.collect {
   case m: MethodSymbol if m.isCaseAccessor => m.name.toString
 }.toList

val values = myClass.productIterator.toList

values.zip(fields).collect {
    case(o: Option[_], field: String) if !allowableFieldNames(field) => assert(o.isEmpty)  
}

This is assuming that productIterator and universe.typeOf[].decls return the parameters and their values in the same order.

From @Dima: Reflection is a kind of a "meta-tool" you resort to (generally, when creating low-level libraries) for a specific reason, to address a specific issue that scala standard toolset does not provide for.

  • Related