Creando un sistema de validaciones con Groovy

Implementación experimental de validaciones

Posted by RubentxuDev on Wednesday, September 27, 2023

Durante mi carrera como ingeniero DevOps, he tenido la oportunidad de explorar una diversidad de lenguajes de programación y paradigmas. Entre ellos, Groovy ha destacado por su potencia y flexibilidad, especialmente en la creación de DSLs (Domain Specific Languages) y la ampliación de la funcionalidad de los tipos existentes a través de la metaprogramación.

Groovy es un lenguaje dinámico para la JVM (Java Virtual Machine) que permite la metaprogramación, un enfoque de programación que trata a los programas como datos. Esto brinda a los desarrolladores la capacidad de modificar el comportamiento de los tipos de datos existentes o incluso introducir nuevas funcionalidades.

Recientemente, trabajé en un proyecto en el que era necesario realizar múltiples validaciones sobre una entrada de datos recogida por la aplicación. Estos datos consistían en tipos de datos básicos de Java, como String, Collection, Map y Number. En lugar de escribir repetidamente las mismas comprobaciones para cada uno de ellos, decidí aprovechar el poder de Groovy para crear una Category que extienda la funcionalidad de estos tipos de datos básicos.

En Groovy, una categoría (Category) es una forma de extender las clases existentes sin modificar directamente su código fuente. Permite agregar métodos y propiedades adicionales a una clase de manera dinámica.

Inicialmente, creé una clase Validator que funciona como un envoltorio (wrapper) para estos tipos de datos, proporcionando una serie de métodos para realizar comprobaciones comunes. Sin embargo, la verdadera belleza de esta solución radica en que la clase Validator permite encadenar estas comprobaciones en un estilo de programación fluido, creando un DSL de validación y recopilando una lista de errores en las validaciones que no se resuelven como verdaderas (true).

Por ejemplo, con esta clase Validator, puedes realizar varias comprobaciones utilizando una API fluida. Aquí tienes un ejemplo:

def number = 5
Boolean result = Validator.from(number)
                    .notNull()
                    .notEqual(3)
                    .isValid()
        
def message = 'hello world'        
String result2 = Validator.from(message).notNull()
                    .equal('hello world')        
                    .throwIfInvalid()
                    
        
def message2 = null    
String result3 = Validator.from(message2)
                    .notNull()   
                    .equal('hello world')        
                    .defaultValueIfInvalid('Default hello world')
                    
def message3 = null    
// Aquí es donde debemos agregar un tag para que los mensajes de error se expresen de manera más clara.
List<String> result4 = Validator.from(message3, 'message3')
                            .notNull()   
                            .equal('hello world')        
                            .errors()
    
println "Resultado 1 isValid $result"
println "Resultado 2 throwIfInvalid $result2"
println "Resultado 3 defaultValueIfInvalid $result3"
println "Resultado 4 errors $result4"

imagen

La parte final isValid, throwIfInvalid, defaultValueIfInvalid y errors se encargan de finalizar el procesamiento de validación y procesar el resultado. Creo que el nombre de la función es autoexplicativo.

Esta implementación no solo mejora la legibilidad y mantenibilidad del código, sino que también reduce la posibilidad de errores.

La clase Validator en este código Groovy actúa como una clase envolvente (wrapper) que proporciona funcionalidades de validación adicionales para diferentes tipos de datos en Groovy, como String, Map, Collection y Number. Estas funcionalidades se detallarán en futuros posts.

Por ejemplo, para un objeto de tipo String, puedes utilizar la clase Validator para realizar comprobaciones como isNull(), notNull(), equal(), notEqual() e isString(), entre otras.

Además, la clase Validator también ofrece funcionalidades para validar si un objeto es de un tipo específico utilizando el método is(Class clazz), y si un objeto cumple una condición específica utilizando el método is(String errorMessage, Closure predicate).

Estas funcionalidades de validación adicionales pueden ser extremadamente útiles en diversos escenarios, como la validación de datos de entrada en una aplicación.

Es importante destacar que las validaciones no se bloquearán por un error provocado por un valor nulo (NullPointerException). El código continuará con el procesamiento de las validaciones y las que no pasen quedarán registradas en una lista errorMessages, que puede utilizarse como reporte de errores.

Aquí te dejo el código de la clase Validator para que lo examines:

import org.codehaus.groovy.runtime.NullObject

class Validator<K extends Validator, T> {
    protected List<Boolean> results
    protected List<String> errorMessages    
    protected T sut // (Subject Under Test)
    protected String tag

    Validator(T sut) {
        this(sut, '')
    }
    
    Validator(T sut, String tag) {
        this.results = []
        this.errorMessages = []
        this.sut = sut
        this.tag = tag
    }
    
    protected K test(String errorMessage, Closure<Boolean> predicate) {
        try {
            if ((sut == null || sut instanceof NullObject)) {
                this.errorMessages.add(errorMessage)
                this.results.add(false)
            } else if (predicate(sut)) {
                this.results.add(true)
            } else {
                this.errorMessages.add(errorMessage)
                this.results.add(false)
            }
        } catch (Exception ex) {
            results.add(false)
            errorMessages.add("Syntax Error, invalid expression. $errorMessage")
        }
        return this as K
    }

    protected Boolean isvalid() {
        return !results.any { it == false }
    }

    T throwIfInvalid(String customErrorMessage = '') {
        errorMessages = errorMessages.plus(0, customErrorMessage, this.tag)
        if (!isvalid()) {
            def message = errorMessages.unique()
            throw new IllegalArgumentException(message)
        }
        return sut
    }

    T defaultValueIfInvalid(T defaultValue) {
        if (isvalid()) {
            return sut
        } else {
            return defaultValue
        }
    }

    def getValue() {
        return sut
    }

    protected String getTagMsg() {
        return tag ? "$tag with value " : ""
    }

    K notNull() {
        K result = test("${tagMsg}${sut} Must not be null") {
            T s -> s != null
        }
        return result
    }

    K isNull() {
        K result = test("${tagMsg}${sut} Must be null") { T s -> s instanceof NullObject || s == null }
        return result
    }

    K notEqual(T n) {
        return test("${tagMsg}${sut} Must be not equal to $n") { T n1 -> n1 != n }
    }

    K equal(T n) {
        return test("${tagMsg}${sut} Must be equal to $n") { T n1 -> n1 == n }
    }
    
    <R> Validator<Validator, R> is(Class<R> clazz) {
        notNull()
        test("${tagMsg}${sut} Must be type $clazz. Current is ${sut?.getClass()?.name}") { T s ->
            s.class == clazz || clazz.isAssignableFrom(s.class)
        }
        return this
    }
    
    <R> Validator<Validator, R> is(String errorMessage, Closure<Boolean> predicate) {
        test(errorMessage, predicate)
        return this
    }
    
    static <T> Validator<Validator, T> from(T sut) {
        return new Validator<Validator, T>(sut)
    }
    
    static <T> Validator<Validator, T> from(T sut, String tag) {
        return new Validator<Validator, T>(sut, tag)
    }
    
    Boolean isValid() {
        return !results.any { it == false }
    }
    
    List<String> errors() {
        this.errorMessages
    }
    
}

Pero este código sería aún más interesante si pudiésemos realizar algo de este estilo:

"hello world".validate("greeter")
            	.isString()
            	.notNull()
            	.moreThan(3)
            	.throwIfInvalid()

def number = 5
Boolean result = number.validate()
                        .notNull()
                        .notEqual(3)
                        .isValid()

En Groovy, puedes usar la metaprogramación para agregar métodos a las clases existentes en tiempo de ejecución. Esto se puede hacer utilizando las categorías de Groovy.

Aquí tienes un ejemplo de cómo podrías implementar este código de validación utilizando categorías en Groovy:

class ValidatorCategory {
    static Validator validate(Object self) {
        return new Validator(self)
    }

    static Validator validate(Object self, String name) {
        return new Validator(self, name)
    }
}

Este código define una categoría de validación que agrega el método validate a todas las clases de objetos. El método validate devuelve un objeto Validator que tiene métodos para verificar si el valor es una cadena, si es nulo y si es más grande que un cierto límite.

A continuación, el código utiliza la categoría de validación para validar una cadena o cualquier objeto. Si la cadena no pasa alguna de las validaciones, se devuelve una lista de errores.

La forma de usar la categoría es la siguiente:

use(ValidatorCategory) {
    def number = 5
    Boolean result = number.validate()
                    .notNull()
                    .notEqual(3)
                    .isValid()

    def message = 'hello world'
    String result2 = message.validate()
                    .notNull()
                    .equal('hello world')
                    .throwIfInvalid()


    def message2 = null
    String result3 = message2.validate()
                    .notNull()
                    .equal('hello world')
                    .defaultValueIfInvalid('Default hello world')

    def message3 = null
    // Aquí es donde debemos agregar un tag para que los mensajes de error se expresen de manera más clara.
    List<String> result4 = message3.validate('message3')
                    .notNull()
                    .equal('hello world')
                    .errors()

    println "Resultado 1 isValid $result"
    println "Resultado 2 throwIfInvalid $result2"
    println "Resultado 3 defaultValueIfInvalid $result3"
	println "Resultado 4 errors $result4"
}

Utilizamos use(ValidatorCategory) y automagicamente 😁 gracias a la metaprogramción de Groovy, todos los objetos que estan dentro de la clausura tendran un metodo validate ya pueda ser un String, un Integer, una List, lo que sea.

En resumen, Groovy es un lenguaje de programación potente y flexible que ofrece una variedad de características avanzadas, como la metaprogramación y las DSLs. La capacidad de Groovy para extender la funcionalidad de los tipos existentes y crear nuevas funcionalidades a través de la metaprogramación puede simplificar y mejorar la calidad del código, haciendo que la programación sea una tarea más eficiente y agradable.

Gracias a la metaprogramación, podemos crear soluciones elegantes y eficientes como la clase Validator que hemos discutido en este post. Esta clase no solo mejora la legibilidad y mantenibilidad del código, sino que también reduce la probabilidad de errores.

Espero que este post te haya proporcionado una visión útil de las capacidades de Groovy y cómo puedes utilizarlas en tus propios proyectos. En futuros posts, exploraremos más características y técnicas de Groovy. ¡Hasta la próxima!