Search code examples
scalascala-cats

Using scala cats to check combination of non-empty fields


In my code i've a class that to check if it's valid i should eval if at least one of the possible field combinations exist (by exist i mean that each field of the combination should not be empty). Example:

case class Test( a: Option[String]
               , b: Option[String]
               , c: Option[String]
               , d: Option[String]
               , e: Option[Double]
               , f: Option[Double])

To be "valid", at least one of the following field combinations must exist ("a,b,c","a,d,e","a,f")

I was trying to do this with scala cats library but i'm kinda lost. Any suggestion would be highly appreciated.


Solution

  • If you want to validate it you can just:

    case class Test( a: Option[String]
                   , b: Option[String]
                   , c: Option[String]
                   , d: Option[String]
                   , e: Option[Double]
                   , f: Option[Double]) {
      // "a,b,c","a,d,e","a,f"
      def isValid = (a.isDefined && b.isDefined && c.isDefined) ||
                    (a.isDefined && d.isDefined && e.isDefined) ||
                    (a.isDefined && f.isDefined)
    }
    

    If you want to make sure that you can only create it if one of field is defined you would have to use a smart constructor

    sealed abstract case class Test private ( a: Option[String]
                                            , b: Option[String]
                                            , c: Option[String]
                                            , d: Option[String]
                                            , e: Option[Double]
                                            , f: Option[Double])
    object Test {
    
      def create( a: Option[String]
                , b: Option[String]
                , c: Option[String]
                , d: Option[String]
                , e: Option[Double]
                , f: Option[Double]): Either[String, Test] =
      if ((a.isDefined && b.isDefined && c.isDefined) ||
          (a.isDefined && d.isDefined && e.isDefined) ||
          (a.isDefined && f.isDefined))
        Right(new Test(a, b, c, d, e, f) {})
      else
        Left("All arguments are empty")
    }
    

    Alternatively use ADT ensuring that one of fields is defined:

    sealed trait Test extends Product with Serializable
    object Test {
      final case class Case1( a: String
                            , b: String
                            , c: String
                            , d: Option[String]
                            , e: Option[Double]
                            , f: Option[Double]) extends Test
      final case class Case2( a: String
                            , b: Option[String]
                            , c: Option[String]
                            , d: String
                            , e: String
                            , f: Option[Double]) extends Test
      final case class Case3( a: String
                            , b: Option[String]
                            , c: Option[String]
                            , d: Option[String]
                            , e: Option[Double]
                            , f: Double) extends Test
    }
    

    You could use Cats here... but for what? You aren't combining a tuple or collection of Options into a single Option. You don't swap F[Option[X] into Option[F[X] or the other way round. There are no side effects, mappings, traversions, constructions of new objects from smaller objects embedded in some context, etc. You can try doing things like

    implicit val booleanMonoid: Monoid[Boolean] = new Monoid[Boolean] {
      def combine(a: Boolean, b: Boolean) = a && b
      def empty = true
    }
    def isValid = List(a, b, c).foldMap(_.isDefined) ||
                  List(a, d, e).foldMap(_.isDefined) ||
                  List(a, f).foldMap(_.isDefined)
    

    or maybe even

    def isValid = (
      (a, b, c).tupled.void orElse (a, d, e).tupled.void orElse (a, f).tupled.void
    ).isDefined
    

    but that is hardly any better than

    def isValid = List(a, b, c).exists(_.isDefined) ||
                  List(a, d, e).exists(_.isDefined) ||
                  List(a, f).exists(_.isDefined)
    

    achievable in vanilla Scala. I guess you could simply the notation by using some Ring defined on Option[_] to use * and +:

    implicit val ring: Ring[Option[_]] = ... // yup, existential type here
    def isValid = ((a * b * c) + (a * d * e) + (a * f)).isDefined
    

    (which would require typelevel algebra) but for using it in just one place I wouldn't see the gain.