Scalaz Validation

Código que debe compilar con scalaz 7.1.3 y con scala 2.11.5

sealed abstract class Validation[E, A] {
    ...
}
case class Success[A](a: A) extends Validation[Nothing, A]
case class Failure[E](e: E) extends Validation[E, Nothing]

La idea es manipular cosas del tipo Validation sin saber si son Success's o Failure's

Validation

sealed abstract class Validation[E, A] {
    ...
}
case class Success[A](a: A) extends Validation[Nothing, A]
case class Failure[E](e: E) extends Validation[E, Nothing]

La idea es manipular cosas del tipo Validation sin saber si son Success's o Failure's

Por debajo si es un Success se continua la computación o si es un Failure se detiene

Validation

import scalaz.Validation

def escalar(d: Double): Validation[String, Int] = {
    if(d >= 0 && d <= 1)
        Validation.success((d*100).toInt)
    else
        Validation.failure(s"Numero debe estar entre 0 y 1 ($d)")
}

def validarProbabilidad(p: Int) = {
    if(p >= 0 && p <= 100)
        Validation.success(p)
    else
        Validation.failure(s"$p no es una probabilidad")
}

def sumarPorcentajes(n1: Int, n2: Int): Validation[String, Int] = {
    validarProbabilidad(n1 + n2)
}

def computo(d1: Double, d2: Double): Validation[String, Double] = {
    for {
        n1 <- escalar(d1)
        n2 <- escalar(d2)
        r <- sumarPorcentajes(n1,n2)
    } yield r / 100.0
}

map y flatMap son "fail-fast". Es decir si se llaman con un objeto que es un Failure, el resultado es ése Failure. Si el objeto es un Success se llama la función pasándole el valor del Success

scala> computo(0.25, 0.5)
res7: scalaz.Validation[String,Double] = Success(0.75)

scala> computo(0.75, 0.25)
res8: scalaz.Validation[String,Double] = Success(1.0)

scala> computo(1.1, 0.0)
res9: scalaz.Validation[String,Double] = Failure(Numero debe estar entre 0 y 1 (1.1))

scala> computo(0.75, 0.26)
res12: scalaz.Validation[String,Double] = Failure(101 no es una probabilidad)

scala> computo(0.0, 1.2)
res10: scalaz.Validation[String,Double] = Failure(Numero debe estar entre 0 y 1 (1.2))

scala> computo(1.1, 1.2)
res11: scalaz.Validation[String,Double] = Failure(Numero debe estar entre 0 y 1 (1.1))

ValidationNel

type ValidationNel[E, X] = Validation[NonEmptyList[E], X]

ValidationNEL es un Validation cuyo tipo a la izquierda es una lista no vacía de errores

type ValidationNel[E, X] = Validation[NonEmptyList[E], X]

ValidationNEL es un Validation cuyo tipo a la izquierda es una lista no vacía de errores

Sirve para acumular errores

ValidationNel

type ValidationNel[E, X] = Validation[NonEmptyList[E], X]

ValidationNEL es un Validation cuyo tipo a la izquierda es una lista no vacía de errores

Sirve para acumular errores

Puede ser útil para reportar más de un error a la vez y no solamente el primer error que se produce

ValidationNel

type ValidationNel[E, X] = Validation[NonEmptyList[E], X]

ValidationNEL es un Validation cuyo tipo a la izquierda es una lista no vacía de errores

Sirve para acumular errores

Puede ser útil para reportar más de un error a la vez y no solamente el primer error que se produce

import scalaz.Validation

...

def computo(d1: Double, d2: Double): Validation[String, Double] = {
    for {
        n1 <- escalar(d1)
        n2 <- escalar(d2)
        r <- sumarPorcentajes(n1,n2)
    } yield r / 100.0
}

ValidationNel

type ValidationNel[E, X] = Validation[NonEmptyList[E], X]

ValidationNEL es un Validation cuyo tipo a la izquierda es una lista no vacía de errores

Sirve para acumular errores

Puede ser útil para reportar más de un error a la vez y no solamente el primer error que se produce

import scalaz.Validation

...

def computo(d1: Double, d2: Double): Validation[String, Double] = {
    for {
        n1 <- escalar(d1)
        n2 <- escalar(d2)
        r <- sumarPorcentajes(n1,n2)
    } yield r / 100.0
}

escalar(d1) y escalar(d2) podrían fallar independientemente

ValidationNel

import scalaz.Validation

...

def computo(d1: Double, d2: Double): Validation[String, Double] = {
    for {
        n1 <- escalar(d1)
        n2 <- escalar(d2)
        r <- sumarPorcentajes(n1,n2)
    } yield r / 100.0
}

escalar(d1) y escalar(d2) podrían fallar independientemente

Con ValidationNel podríamos reportar que escalar(d1) y escalar(d2) fallaron

ValidationNel

import scalaz.Validation

...

def computo(d1: Double, d2: Double): Validation[String, Double] = {
    for {
        n1 <- escalar(d1)
        n2 <- escalar(d2)
        r <- sumarPorcentajes(n1,n2)
    } yield r / 100.0
}

escalar(d1) y escalar(d2) podrían fallar independientemente

Con ValidationNel podríamos reportar que escalar(d1) y escalar(d2) fallaron

En vez de utilizar flatMap (que es "fail-fast") podemos usar |@| (que acumula errores)

ValidationNel

ValidationNel

import scalaz.ValidationNel
import scalaz.syntax.applicative._

def computo(d1: Double, d2: Double): ValidationNel[String, Double] = {
    (escalar(d1).toValidationNel |@| escalar(d2).toValidationNel) { (d1,d2) => 
        d1 + d2 
    } flatMap { s =>
        validarProbabilidad(s).toValidationNel
    } map { s =>
        s / 100.0
    }
}

ValidationNel

import scalaz.ValidationNel
import scalaz.syntax.applicative._

def computo(d1: Double, d2: Double): ValidationNel[String, Double] = {
    (escalar(d1).toValidationNel |@| escalar(d2).toValidationNel) { (d1,d2) => 
        d1 + d2 
    } flatMap { s =>
        validarProbabilidad(s).toValidationNel
    } map { s =>
        s / 100.0
    }
}
def computo(d1: Double, d2: Double): ValidationNel[String, Double] = {
    val sumaVal = (escalar(d1).toValidationNel |@| escalar(d2).toValidationNel) { (d1,d2) => 
        d1 + d2 
    }
    for {
        suma <- sumaVal
        s <- validarProbabilidad(suma).toValidationNel
    } yield (s / 100.0)
}

Utilizando for-comprehension:

ValidationNel

import scalaz.ValidationNel
import scalaz.syntax.applicative._

def computo(d1: Double, d2: Double): ValidationNel[String, Double] = {
    (escalar(d1).toValidationNel |@| escalar(d2).toValidationNel) { (d1,d2) => 
        d1 + d2 
    } flatMap { s =>
        validarProbabilidad(s).toValidationNel
    } map { s =>
        s / 100.0
    }
}
def computo(d1: Double, d2: Double): ValidationNel[String, Double] = {
    val sumaVal = (escalar(d1).toValidationNel |@| escalar(d2).toValidationNel) { (d1,d2) => 
        d1 + d2 
    }
    for {
        suma <- sumaVal
        s <- validarProbabilidad(suma).toValidationNel
    } yield (s / 100.0)
}

Utilizando for-comprehension:

Para poder utilizar la sintáxis de |@| toca hacer este import

ValidationNel

scala> computo(0.25, 0.5)
res18: scalaz.ValidationNel[String,Double] = Success(0.75)

scala> computo(0.75, 0.25)
res19: scalaz.ValidationNel[String,Double] = Success(1.0)

scala> computo(1.1, 0.0)
res20: scalaz.ValidationNel[String,Double] = Failure(NonEmptyList(Numero debe estar entre 0 y 1 (1.1)))

scala> computo(0.75, 0.26)
res21: scalaz.ValidationNel[String,Double] = Failure(NonEmptyList(101 no es una probabilidad))

scala> computo(0.0, 1.2)
res22: scalaz.ValidationNel[String,Double] = Failure(NonEmptyList(Numero debe estar entre 0 y 1 (1.2)))

scala> computo(1.1, 1.2)
res23: scalaz.ValidationNel[String,Double] = Failure(NonEmptyList(Numero debe estar entre 0 y 1 (1.1), Numero debe estar entre 0 y 1 (1.2)))

En Wesurance

case class InvalidParameterValue(msg: String) extends WesuranceException(msg)
case class MultipleInvalidParameters(msg: String, errors: NonEmptyList[InvalidParameterValue]) extends WesuranceException(msg)

En Wesurance

case class InvalidParameterValue(msg: String) extends WesuranceException(msg)
case class MultipleInvalidParameters(msg: String, errors: NonEmptyList[InvalidParameterValue]) extends WesuranceException(msg)
package co.com.wesurance.commons

import scalaz.{Validation, ValidationNel}

package object exceptions {

  type WesuranceValidationNEL[A] = ValidationNel[InvalidParameterValue, A]

  /**
   * Constructores
   */
  object WesuranceValidationNEL {
    def success[A](a: A) = Validation.success(a)

    def failure(msg: String) = Validation.failureNel(InvalidParameterValue(msg))

    def getOrThrowException[A](validation: WesuranceValidationNEL[A], msg: String): A = {
      validation.valueOr { errors =>
        if(errors.size == 1)
          throw errors.head
        else
          throw MultipleInvalidParameters(msg, errors)
      }
    }
  }

}

En Wesurance

case class InvalidParameterValue(msg: String) extends WesuranceException(msg)
case class MultipleInvalidParameters(msg: String, errors: NonEmptyList[InvalidParameterValue]) extends WesuranceException(msg)
package co.com.wesurance.commons

import scalaz.{Validation, ValidationNel}

package object exceptions {

  type WesuranceValidationNEL[A] = ValidationNel[InvalidParameterValue, A]

  /**
   * Constructores
   */
  object WesuranceValidationNEL {
    def success[A](a: A) = Validation.success(a)

    def failure(msg: String) = Validation.failureNel(InvalidParameterValue(msg))

    def getOrThrowException[A](validation: WesuranceValidationNEL[A], msg: String): A = {
      validation.valueOr { errors =>
        if(errors.size == 1)
          throw errors.head
        else
          throw MultipleInvalidParameters(msg, errors)
      }
    }
  }

}

Type alias con nuestro tipo de error a la izquierda

En Wesurance

case class InvalidParameterValue(msg: String) extends WesuranceException(msg)
case class MultipleInvalidParameters(msg: String, errors: NonEmptyList[InvalidParameterValue]) extends WesuranceException(msg)
package co.com.wesurance.commons

import scalaz.{Validation, ValidationNel}

package object exceptions {

  type WesuranceValidationNEL[A] = ValidationNel[InvalidParameterValue, A]

  /**
   * Constructores
   */
  object WesuranceValidationNEL {
    def success[A](a: A) = Validation.success(a)

    def failure(msg: String) = Validation.failureNel(InvalidParameterValue(msg))

    def getOrThrowException[A](validation: WesuranceValidationNEL[A], msg: String): A = {
      validation.valueOr { errors =>
        if(errors.size == 1)
          throw errors.head
        else
          throw MultipleInvalidParameters(msg, errors)
      }
    }
  }

}

Type alias con nuestro tipo de error a la izquierda

Constructores utilitarios

En Wesurance

case class InvalidParameterValue(msg: String) extends WesuranceException(msg)
case class MultipleInvalidParameters(msg: String, errors: NonEmptyList[InvalidParameterValue]) extends WesuranceException(msg)
package co.com.wesurance.commons

import scalaz.{Validation, ValidationNel}

package object exceptions {

  type WesuranceValidationNEL[A] = ValidationNel[InvalidParameterValue, A]

  /**
   * Constructores
   */
  object WesuranceValidationNEL {
    def success[A](a: A) = Validation.success(a)

    def failure(msg: String) = Validation.failureNel(InvalidParameterValue(msg))

    def getOrThrowException[A](validation: WesuranceValidationNEL[A], msg: String): A = {
      validation.valueOr { errors =>
        if(errors.size == 1)
          throw errors.head
        else
          throw MultipleInvalidParameters(msg, errors)
      }
    }
  }

}

Type alias con nuestro tipo de error a la izquierda

Constructores utilitarios

Para extraer el valor o lanzar una excepción

En Wesurance

def validateTextWithoutHTMLelements(text: String): WesuranceValidationNEL[String] = {
    if(text.matches(htmlElemRegex))
        WesuranceValidationNEL.failure(s"The text can't contain HTML elements")
    else
        WesuranceValidationNEL.success(text)
}
def validatePlainTextWithoutUrls(text: String): WesuranceValidationNEL[String] = {
    if(!text.matches(urlRegex))
        WesuranceValidationNEL.success(text)
    else
        WesuranceValidationNEL.failure("The text can't contain URLs")
}

A partir de validaciones pequeñas:

En Wesurance

def validateTextWithoutHTMLelements(text: String): WesuranceValidationNEL[String] = {
    if(text.matches(htmlElemRegex))
        WesuranceValidationNEL.failure(s"The text can't contain HTML elements")
    else
        WesuranceValidationNEL.success(text)
}
def validatePlainTextWithoutUrls(text: String): WesuranceValidationNEL[String] = {
    if(!text.matches(urlRegex))
        WesuranceValidationNEL.success(text)
    else
        WesuranceValidationNEL.failure("The text can't contain URLs")
}
def validateCommunityName(name: String): WesuranceValidationNEL[String] = {
    val v1 = TextValidation.validatePlainTextWithoutUrls(name)
    val v2 = TextValidation.validateTextWithoutHTMLelements(name)
    (v1 |@| v2) { case _ => name } leftMap { errors =>
      errors.map(error => InvalidParameterValue(s"The community's name is invalid: ${error.msg}") )
    }
}

A partir de validaciones pequeñas:

Vamos construyendo validaciones mas grandes:

En caso de que ambas validaciones sean exitosas devolvemos un Success(name)

En Wesurance

def validateTextWithoutHTMLelements(text: String): WesuranceValidationNEL[String] = {
    if(text.matches(htmlElemRegex))
        WesuranceValidationNEL.failure(s"The text can't contain HTML elements")
    else
        WesuranceValidationNEL.success(text)
}
def validatePlainTextWithoutUrls(text: String): WesuranceValidationNEL[String] = {
    if(!text.matches(urlRegex))
        WesuranceValidationNEL.success(text)
    else
        WesuranceValidationNEL.failure("The text can't contain URLs")
}
def validateCommunityName(name: String): WesuranceValidationNEL[String] = {
    val v1 = TextValidation.validatePlainTextWithoutUrls(name)
    val v2 = TextValidation.validateTextWithoutHTMLelements(name)
    (v1 |@| v2) { case _ => name } leftMap { errors =>
      errors.map(error => InvalidParameterValue(s"The community's name is invalid: ${error.msg}") )
    }
}

A partir de validaciones pequeñas:

Vamos construyendo validaciones mas grandes:

leftMap es una especie de catch para volver a tirar la "excepción" pero transformándola

En Wesurance

def validateTextWithoutHTMLelements(text: String): WesuranceValidationNEL[String] = {
    if(text.matches(htmlElemRegex))
        WesuranceValidationNEL.failure(s"The text can't contain HTML elements")
    else
        WesuranceValidationNEL.success(text)
}
def validatePlainTextWithoutUrls(text: String): WesuranceValidationNEL[String] = {
    if(!text.matches(urlRegex))
        WesuranceValidationNEL.success(text)
    else
        WesuranceValidationNEL.failure("The text can't contain URLs")
}
def validateCommunityName(name: String): WesuranceValidationNEL[String] = {
    val v1 = TextValidation.validatePlainTextWithoutUrls(name)
    val v2 = TextValidation.validateTextWithoutHTMLelements(name)
    (v1 |@| v2) { case _ => name } leftMap { errors =>
      errors.map(error => InvalidParameterValue(s"The community's name is invalid: ${error.msg}") )
    }
}

A partir de validaciones pequeñas:

Vamos construyendo validaciones mas grandes:

En caso de que haya un error cambiamos el mensaje para que señale específicamente que se trata de un error con el nombre de la comunidad (y no con un texto cualquiera)

En Wesurance

case class NewCommunityData(
                            name: String,
                            description: String,
                            categoryId: Int,
                            protectionsIds: List[Int],
                            riskObjectId: Int,
                            membersNeeded: Int,
                            casesCovered: Int,
                            compensation: Float
) {

    def validate(): WesuranceValidationNEL[NewCommunityData] = {
        (
            Community.validateMembersNeeded(membersNeeded) |@|
            Community.validateCasesCovered(casesCovered) |@|
            Community.validateCompensationValue(compensation) |@|
            Community.validateCommunityName(name) |@|
            Community.validateCommunityDescription(description)
        ){ case _ => this }
    }

}

Múltiples validaciones independientes

En caso de que todas las validaciones sean exitosas devolvemos un Success(NewCommunityData)

Made with Slides.com