Validaciones con Groovy Cap.2

Extendiendo el laboratorio anterior con validaciones en Groovy

Posted by RubentxuDev on Sunday, October 8, 2023

Validaciones Avanzadas con Groovy: Tipos Básicos y Soporte Multilenguaje

En nuestro anterior post vimos cómo se puede crear un sistema de validaciones con Groovy y metaprogramación. Hoy vamos a expandir esa idea y ver cómo podemos hacer validaciones básicas para tipos básicos como Map, Collection, Number y String. Además, vamos a introducir soporte multilenguaje para los mensajes de errores.

Recordatorio Rápido

Para los que no recuerdan, teníamos una clase Validator y una clase ValidatorCategory. La primera es una clase genérica que se utiliza para validar objetos de cualquier tipo. Esta tiene métodos que permiten realizar varias comprobaciones sobre el objeto que se está validando.

Por otro lado, la clase ValidatorCategory es una clase que extiende las funcionalidades de cualquier objeto en Groovy, añadiendo los métodos validate que crean una nueva instancia de la clase Validator para el objeto.

esquema

Validador de Cadenas

Por no hacer muy largo este post nos centramos en el código minimo necesario para extender la funcionalidad sobre tipos String.

Empezamos extendiendo la funcionalidad de la clase Validator con StringValidator, que es más específica para el tipo básico de cadena. A continuación se muestra un ejemplo de cómo se podría implementar:

import org.codehaus.groovy.runtime.NullObject

class StringValidator extends Validator<StringValidator, String> {
    def EMAIL_REGEX = /^(([^<>()[\]\.,;:\s@\"]]+(\.[^<>()[\]\.,;:\s@\"]]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
    def HTTP_PROTOCOL_REGEX = /^(?:http[s]?:\/\/.)(?:www\.)?[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)/

    private StringValidator(String text, String tag = '' ) {
        super(text, tag)
    }

    private StringValidator(NullObject text, String tag = '' ) {
        super(text, tag)
    }

    StringValidator(Validator validation) {
        super(validation.sut, validation.tag)
        this.validationResults = validation.validationResults
        this.tag = validation.tag
    }

    StringValidator moreThan(int size) {
        return test(renderMessage('moreThan', tagMsg, sut, size)) { String s -> s.length() >= size }
    }

    StringValidator lessThan(int size) {
        return test(renderMessage('lessThanString', tagMsg, sut, size)) { String s -> s.length() <= size }
    }

    StringValidator between(int minSize, int maxSize) {
        moreThan(minSize)
        return lessThan(maxSize)
    }

    StringValidator  contains(String subString) {
        return test(renderMessage('contains', tagMsg, sut, subString)) { String s -> s.contains(subString) }
    }

    StringValidator isEmail() {
        return test(renderMessage('isEmail', tagMsg, sut)) { String s -> s ==~ EMAIL_REGEX }
    }

    StringValidator matchRegex(regex) {
        return test(renderMessage('matchRegex', tagMsg, sut, regex)) { String s -> s ==~ regex }
    }

    StringValidator isHttpProtocol() {
        return test(renderMessage('isHttpProtocol', tagMsg, sut)) { String s -> s ==~ HTTP_PROTOCOL_REGEX }
    }

    StringValidator containsIn(List<String> array) {
        return test(renderMessage('containsIn', tagMsg, sut, array.join(','))) { String s -> array.contains(s) }
    }

    StringValidator notEmpty() {
        return test(renderMessage('notEmpty', tagMsg, sut)) { s -> s != null && s != '' }
    }

    static StringValidator from(String text) {
        return new StringValidator(text)
    }

    static StringValidator from(String text, String tag) {
        return new StringValidator(text, tag)
    }

}

developer

En este código, hemos definido varias validaciones específicas para cadenas, como comprobar si es un correo electrónico válido, si contiene un subconjunto de cadena específico, si está dentro de un rango de tamaño específico, entre otros.

Luego, extendemos nuestra ValidatorCategory para que pueda añadir funcionalidad sobre los tipos String de Java. Esto nos permitirá usar nuestro StringValidator directamente en cualquier cadena.

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

    static Validator validate(Object self, String name) {
        return new Validator(self, name)
    }
    
    static StringValidator validate(String self, String name= '') {
        return StringValidator.from(self, tag)
    }
} 

Ahora, podemos realizar validaciones de cadenas de la siguiente manera:

 use(ValidatorCategory) {
 	def result =  "hello@email.com".validate()
                               .notEmpty()
                               .contains("@")
                               .between(5,20)
                               .isEmail()
                               .errors()

 	assert result.size() == 0 :  "No deben existir errores"

 
 	Boolean result2 =  "http://example.com/resource".validate()
                    								.notEmpty()
                    								.isHttpProtocol()
                    								.moreThan(10)
                                                    .matchRegex('.*resource$')
                                                    .isValid()
 
 
   def result3 =  "manager".validate()
                            .notEmpty()
                            .containsIn(["manager", "admin", "user"])
                            .defaultValueIfInvalid("user")
 
   
   assert result.size() == 0 :  "Result No deben existir errores"
   assert result2 :  "Result2 No deben existir errores"
   assert result3 == 'manager':  "Result3 No deben existir errores"  
 }
   
  
  

Soporte Multilenguaje

Ahora vamos a darle a nuestro sistema de validación la capacidad de mostrar mensajes de error en diferentes idiomas. Para ello, haremos algunos cambios en la clase Validator.groovy

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


    protected Validator(T sut) {
        this(sut,'')
    }

    protected Validator(T sut, String tag) {
        this.results = []
        this.errorMessages = []
        this.sut = sut
        this.tag = tag
        Locale locale = Locale.getDefault()
        resourceBundle = ResourceBundle.getBundle("i18n/messages", locale)

    }

    protected String renderMessage(String key, Object... params) {
        return MessageFormat.format(resourceBundle.getString(key), params)
    }
    
    // ... Resto de la clase
        
}

Los mensajes se extraen de archivos de propiedades para cada tipo de lenguaje usando ResourceBundle de Java.

ResourceBundle es una clase en Java que se utiliza para internacionalizar (i18n) una aplicación. Esto significa que puede adaptar su programa para ser utilizado en diferentes regiones o lenguajes. ResourceBundle le permite mover los textos localizables fuera de su código fuente y almacenarlos en archivos de propiedades.

Un archivo de propiedades es un archivo de texto simple que contiene pares de clave-valor, donde la clave es el identificador del texto y el valor es el texto en sí.

Por ejemplo, supongamos que tenemos una aplicación que queremos localizar en inglés y español. Podríamos tener dos archivos de propiedades, uno para cada idioma:

Ejemplo:

messages_en.properties

exception=Syntax Error, invalid expression. {0}
getTagMsg={0} with value
notNull={0} {1} Must not be null
isNull= {0} {1} Must be null
notEqual={0} {1} Must be not equal to {2}
equal={0} {1} Must be equal to {2}
isString={0} {1} Must be type String.
isNumber={0} {1} Must be type Number.
isList=Must be type List. Current is {0}
isCollection=Must be type List. Current is {0}
isMap={0} {1} Must be type Map. Current is {2}
is={0} {1} Must be type $clazz. Current is {2}
withExpression=The expression did not evaluate to true{0} with expression '{1}'
moreThan={0} {1} Must have more than {2} chars.
lessThanString={0} {1} Must have less than {2} chars.
contains={0} {1} Must contain {2}
isEmail={0} {1} Must be email type
matchRegex={0} {1} Must be match Regular Expression '/{2}/'
isHttpProtocol={0} {1} Must be http protocol type
containsIn={0} {1} Must contain in {2}
notEmpty={0} {1} Must not be empty
... Resto de properties

messages_es.properties

exception=Error de sintaxis, expresión no válida. {0}
getTagMsg={0} con valor
notNull={0} {1} No debe ser nulo
isNull= {0} {1} Debe ser nulo
notEqual={0} {1} No debe ser igual a {2}
equal={0} {1} Debe ser igual a {2}
isString={0} {1} Debe ser de tipo Cadena.
isNumber={0} {1} Debe ser de tipo Número.
isList=Debe ser del tipo Lista. La corriente es {0}
isCollection=Debe ser del tipo Lista. La corriente es {0}
isMap={0} {1} Debe ser del tipo Mapa. La corriente es {2}
is={0} {1} Debe ser del tipo $clazz. La corriente es {2}
withExpression=La expresión no se evaluó como verdadera{0} con la expresión '{1}'
moreThan={0} {1} Debe tener más de {2} caracteres.
lessThanString={0} {1} Debe tener menos de {2} caracteres.
contains={0} {1} Debe contener {2}
isEmail={0} {1} Debe ser un tipo de correo electrónico
matchRegex={0} {1} Debe coincidir con la expresión regular '/{2}/'
isHttpProtocol={0} {1} Debe ser el tipo de protocolo http
containsIn={0} {1} Debe contener en {2}
notEmpty={0} {1} No debe estar vacío
... Resto de properties

Conclusión

Con estos cambios, nuestro sistema de validación ahora es capaz de realizar validaciones más específicas basadas en el tipo de los objetos y de mostrar mensajes de error en diferentes idiomas. Esto puede ser extremadamente útil en aplicaciones que necesitan validar muchos tipos de datos y que se utilizan en diferentes regiones del mundo.

En el siguiente post, veremos cómo podemos seguir extendiendo este sistema para añadir más tipos de validaciones y funcionalidades. ¡Nos vemos en la próxima!


Puedes encontrar el código completo del sistema de validación en este enlace al repositorio.