Validated: control de errores

En toda aplicación software es necesario realizar tareas de validación de campos, validación de formularios,o bien, verificación de una función;y, una vez realizadas las validaciones individuales, es necesario realizar la validación del conjunto de todas ellas. En la entrada de hoy, Validated: control de errores, me centraré en describir y realizar un ejemplo de validación de los campos de un formulario representado en una estructura de tipo Map.

Supongamos que estamos desarrollando una aplicación en la cual tenemos un formulario de dos elementos: el primero, el campo nombre; y, el segundo, el campo edad. Las validaciones que tenemos que realizar son: validación del campo nombre, validación del campo edad y evaluación de la validación del conjunto del formulario.

El campo nombre debe de ser un campo que no sea vacío. El campo edad debe de ser un
campo no vacío, entero y mayor que cero.

Las validaciones se realizan mediante el tipo Validated el cual no está definido como un tipo estándar de Scala, está definido en la librería Cats. La importación del tipo Validated y el resto de importaciones necesarias para el ejemplo son las siguientes:

import cats.data.Validated
import cats.instances.list._
import cats.syntax.all._

El primer paso a realizar es realizar la definición de los alias de todas las estructuras sobre las que trabajaremos. Los alias son los siguientes:

  • Definición del formulario. El formulario es una estructura de tipo Map.
type Form = Map[String, String]
  • Definición de la estructura de control de error sencillas. Las verificaciones
    sencillas se realizarán con un elemento de tipo Either
type ControlErrorFast[A] = Either[List[String], A]
  • Definición de la estructura de control de error compleja o validación. Las verificaciones complejas se realizarán con un elemento de tipo Validated.
type ValidatedForm[A] = Validated[List[String], A]

La primera operación a realizar es la obtención de un elemento del formulario. Para
realizar dicha operación, definiremos la función getValue de la siguiente manera:

def getValue(form: Form)(campo: String) : ControlErrorFast[String] = 
  form.get(campo)
    .toRight(List(s"El valor de $campo no está especificado"))

La segunda operación es realizar la verificación del campo nombre. Para realizar dicha operación, definimos la función readName y sus funciones auxiliares. El código es el siguiente:

def nonBlank(nombre:String)(dato:String): ControlErrorFast[String] =
   Right(dato)
    .ensure(List(s"El campo $nombre no debe de ser vacío."))
     (_.nonEmpty)

def readName(form: Form): ControlErrorFast[String] =
  getValue(form)("name")
   .flatMap( elem => nonBlank("name")(elem))

def nonBlank(nombre:String)(dato:String): ControlErrorFast[String] =
  Right(dato) 
   .ensure(List(s"El campo $nombre no debe de ser vacío."))
    (_.nonEmpty)

La tercera operación es realizar la verificación del campo edad. Para realizar dicha
operación, definiremos la función readAge y sus funciones auxiliares. El código es el
siguiente:

def readAge(form: Form): ControlErrorFast[Int] =
  getValue(form)("age")
   .flatMap(nonBlank("age"))
   .flatMap(parseInt("age"))
   .flatMap(nonNegative("age"))

def parseInt(nombre:String)(age:String): ControlErrorFast[Int] =
  Either.catchOnly[NumberFormatException](age.toInt)
   .leftMap(_ => List(s"El campo $nombre debe de ser numérico"))

def nonNegative(nombre:String)(dato:Int): ControlErrorFast[Int] =
  Right(dato)
   .ensure(List(s"El campo $nombre no es válido"))
    (_ >= 0 )

Para finalizar, una vez realizadas las verificaciones de los campos, es necesario realizar la validación del formulario para obtener el listado de los posibles errores presentes en el formulario, o bien, retornar el resultado final.

La validación del formulario se realiza utilizando elementos de tipo Validated y un elemento que trabaje con los contextos de validación; dicho elemento, es un Semigroupal el cual genera una tupla de elementos del mismo contexto.

val formHtml: Form = Map("name" -> "Pepito", "age" -> "40")
val valid1_1:ValidatedForm[String] = Validated.fromEither(readName(formHtml))
val valid1_2:ValidatedForm[Int] = Validated.fromEither(readAge(formHtml))
val resultado1 = (valid1_1, valid1_2).tupled
println(s"resultado1=${resultado1}")

La salida por consola es la siguiente:

resultado1=Valid((Pepito,40))

Para el supuesto de trabajar con un formulario con datos erróneos, el código sería el siguiente:

val formHtmlKO3: Form = Map("name" -> "", "age" -> "-1")
val valid4_1:ValidatedForm[String] = Validated.fromEither(readName(formHtmlKO3))
val valid4_2:ValidatedForm[Int] = Validated.fromEither(readAge(formHtmlKO3))
val resultado4 = (valid4_1, valid4_2).tupled
println(s"resultado4=${resultado4}")
println

La salida por consola es la siguiente:

resultado4=Invalid(List(El campo name no debe de ser vacío., El campo age no es válido))

Como conclusión final, una de las opciones para el control de errores es utilizar el componente Either, pudiendo controlar los valores y las excepciones. Cuando todos los elementos Either son agrupados para su evaluación con Validated y un Semigropal podemos obtener el resultado, o bien, el conjunto de los mensajes de las no validaciones que se han producido en el mismo instante. A diferencia de otros procesos de validación, la ventaja reside en que conocemos todos los fallos producidos en el mismo tiempo.