apux

apux

Publicado
Septiembre 24, 2013

Próximos Eventos

Blog

Parte I: Creación de validadores para CURP y RFC

Una de características más potentes del ecosistema Ruby es la facilidad de compartir bibliotecas con funcionalidades que pueden ser (re)utilizadas en varios proyectos mediante gemas. La creación de gemas puede parecer una labor muy complicada y reservada sólo para los gurús de Ruby, pero no es así. Es posible crear nuestras propias gemas sin demasiado esfuerzo.

Este tutorial se compone de tres partes, en la primera parte crearemos los validadores para la Clave Única de Registro de Población (CURP) y el Registro Federal de Contribuyentes (RFC) que son los formatos más comunes de identificación en México, e integraremos estos nuevos validadores a la lista de validadores que ya incorpora ActiveRecord. En la segunda parte veremos cómo crear una gema con estos validadores y en la tercera parte veremos cómo publicar la gema e integrarla a una aplicación Rails.

En caso que deseen reproducir el ejemplo, las versiones de las herramientas que utilizaremos en este tutorial son: Ruby 2.0, Rails 4.0, Bundler 1.3.5, RubyGems 2.0.7 y RSpec 2.14.

Parte I: Creación de validadores para CURP y RFC

Objetivo

Se pretende crear validadores que permitan escribir la validación de CURP y RFC de la siguiente manera:

validates :rfc, rfc_format: true
validates :curp, curp_format: true

Aplicación Rails

Lo primero que haremos es crear una aplicación Rails con un modelo llamado Empleado, que incluya los campos curp y rfc.

rails new my_app -T
cd my_app

La opción -T indica que no usaremos el framework de pruebas por omisión, así que antes de generar el modelo, instalamos la gema de rspec-rails que es la que vamos a ocupar en el proyecto. La agregamos como dependencia al Gemfile

group :development, :test do
  gem 'rspec-rails', '~> 2.0'
end

y la instalamos por medio de bundler

bundle install

Ahora sí, podemos crear nuestro modelo y correr las migraciones de nuestra base de datos.

rails generate scaffold empleado curp rfc
rake db:migrate

Pruebas

Ahora, crearemos las pruebas para definir el comportamiento de nuestras validaciones. Existen varias herramientas para hacer TDD/BDD en Ruby, como TestUnit, MiniTest, Shoulda, Cucumber, Bacon, etc. En nuestro caso, usaremos RSpec ya que es una herramienta muy potente, bastante difundida entre los desarrolladores Rails y fácil de utilizar. Si no estás familiarizado con RSpec, las siguientes ligas pueden resultarte útiles: info, rspec expectations, better specs y el libro de RSpec

Ya instalada la gema, necesitamos generar el archivo spec/spec_helper.rb. Lo podemos crear desde cero o bien utilizar el generador que la gema nos proporciona. Por comodidad, haremos esto último.

rails generate rspec:install

De esta manera, se genera el archivo spec/spec_helper.rb con el contenido necesario para nuestra aplicación.

Specs

El comportamiento que esperamos para los campos de curp y rfc es el siguiente:

#spec/models/empleado_spec.rb
require 'spec_helper'

describe Empleado do
  describe '#rfc' do
    context 'with valid data' do
      it 'accepts rfc (completo)' do
        expect(Empleado.new(rfc: 'AAAA111111AAA')).to have(:no).errors_on(:rfc)
      end

      it 'accepts rfc (con 3 caracteres para el nombre)' do
        expect(Empleado.new(rfc: 'AAA111111AAA')).to have(:no).errors_on(:rfc)
      end

      it 'accepts rfc (sin homoclave)' do
        expect(Empleado.new(rfc: 'AAAA111111')).to have(:no).errors_on(:rfc)
      end

      it 'accepts rfc (sin homoclave y con 3 caracteres para el nombre)' do
        expect(Empleado.new(rfc: 'AAA111111')).to have(:no).errors_on(:rfc)
      end

      it 'accepts rfc (datos de nombre con Ñ)' do
        expect(Empleado.new(rfc: 'ÑAAA111111AAA')).to have(:no).errors_on(:rfc)
      end

      it 'accepts rfc (datos de nombre con &)' do
        expect(Empleado.new(rfc: 'A&A111111AAA')).to have(:no).errors_on(:rfc)
      end
    end

    context 'with invalid data' do
      it 'refuses rfc (nombre con digito en lugar de letra)' do
        expect(Empleado.new(rfc: '9AAA111111')).to have(1).error_on(:rfc)
      end

      it 'refuses rfc (caracter invalido)' do
        expect(Empleado.new(rfc: 'A*AA111111')).to have(1).error_on(:rfc)
      end

      it 'refuses rfc (falta un digito en la fecha)' do
        expect(Empleado.new(rfc: 'AAAA11111')).to have(1).error_on(:rfc)
      end

      it 'refuses rfc (falta un caracter en los datos del nombre)' do
        expect(Empleado.new(rfc: 'AA111111')).to have(1).error_on(:rfc)
      end

      it 'refuses rfc (el dia es invalido 42)' do
        expect(Empleado.new(rfc: 'AAAA111142')).to have(1).error_on(:rfc)
      end

      it 'refuses rfc (el mes es invaido 25)' do
        expect(Empleado.new(rfc: 'AAAA112511')).to have(1).error_on(:rfc)
      end
    end
  end

  describe '#curp' do
    context 'with valid data' do
      it 'accepts curp (hombre)' do
        expect(Empleado.new(curp: 'AAAA111111HDFBBB01')).to have(:no).errors_on(:curp)
      end

      it 'accepts curp (mujer)' do
        expect(Empleado.new(curp: 'AAAA111111MDFBBB01')).to have(:no).errors_on(:curp)
      end

      it 'accepts curp (digito anti-duplicado alfanumérico)' do
        expect(Empleado.new(curp: 'AAAA111111HDFBBBA1')).to have(:no).errors_on(:curp)
      end
    end

    context 'with invalid data' do
      it 'refuses curp (nombre con digito en lugar de letra)' do
        expect(Empleado.new(curp: '9AAA111111HDFBBB01')).to have(1).error_on(:curp)
      end

      it 'refuses curp (caracter invalido)' do
        expect(Empleado.new(curp: 'A*AA111111HDFBBB01')).to have(1).error_on(:curp)
      end

      it 'refuses curp (falta un digito en la fecha)' do
        expect(Empleado.new(curp: 'AAAA11111HDFBBB01')).to have(1).error_on(:curp)
      end

      it 'refuses curp (falta un caracter en los datos del nombre)' do
        expect(Empleado.new(curp: 'AAA111111HDFBBB01')).to have(1).error_on(:curp)
      end

      it 'refuses curp (el dia es invalido 42)' do
        expect(Empleado.new(curp: 'AAAA111142HDFBBB01')).to have(1).error_on(:curp)
      end

      it 'refuses curp (el mes es invaido 25)' do
        expect(Empleado.new(curp: 'AAAA112511HDFBBB01')).to have(1).error_on(:curp)
      end

      it 'refuses curp (sexo es inválido K)' do
        expect(Empleado.new(curp: 'AAAA111111KDFBBB01')).to have(1).error_on(:curp)
      end

      it 'refuses curp (caracteres de consonantes tiene alguna vocal)' do
        expect(Empleado.new(curp: 'AAAA111111HDFABB01')).to have(1).error_on(:curp)
        expect(Empleado.new(curp: 'AAAA111111HDFBAB01')).to have(1).error_on(:curp)
        expect(Empleado.new(curp: 'AAAA111111HDFBBA01')).to have(1).error_on(:curp)
      end

      it 'refuses curp (digito verificador alfanumérico)' do
        expect(Empleado.new(curp: 'AAAA111111HDFBBB0A')).to have(1).error_on(:curp)
      end
    end
  end
end

El archivo puede parecer extenso, pero es realmente muy sencillo. Lo único que hemos incluido son algunos ejemplos que verifican que se acepten RFC's y CRURP's válidos y se rechacen los inválidos. Por supuesto que este conjunto de pruebas se puede incrementar para agregar más casos, pero por el momento podemos darnos por satisfechos con los que tenemos.

NOTA: Los que ya utilizan RSpec habrán notado que hemos utilizado la sintaxis de expect en lugar de should. Personalmente me gusta más la sintaxis de should porque es más elegante y más fácil de leer, pero actualmente se recomienda usar expect ya que será la sintaxis por omisión para RSpec 3.

Para ejecutar nuestras pruebas, es necesario configurar nuestra base de datos de prueba:

rake db:test:prepare

Ahora, ejecutamos nuestro conjunto de pruebas.

rspec spec/models


Finished in 0.02662 seconds
24 examples, 15 failures

Muchas de las pruebas fallan. Algunas otras pasan, pero en realidad son falsos positivos, ya que no hemos escrito la funcionalidad aún. Las pruebas que pasan son las que validan que un valor en particular sea aceptado, y como aún no agregamos las validaciones, todos los valores son aceptados, así que, lo que haremos a continuación será agregar esa funcionalidad.

Expresiones regulares

Deberíamos detenernos un momento para solucionar el problema de la validación en sí: generar las expresiones regulares que empaten con el formato de la CURP y el RFC. Considero que no es necesario entrar en detalles sobre las reglas para generar y validar la CURP y el RFC, si se necesita más información al respecto, es posible consultarla en la wikipedia (curp, rfc) y en la conducef (curp, rfc)

Las expresiones regulares generadas son las siguientes:

# curp
/\A[A-Z][AEIOUX][A-Z]{2}[0-9]{2}[0-1][0-9][0-3][0-9][MH][A-Z][BCDFGHJKLMNÑPQRSTVWXYZ]{4}[0-9A-Z][0-9]\z/i
# rfc
/\A[A-ZÑ&]{3,4}[0-9]{2}[0-1][0-9][0-3][0-9]([A-Z0-9]{3})?\z/i

No nos detendremos mucho para analizar estas expresiones, si no estás familiarizado con expresiones regulares en Ruby, puedes revisar estas ligas: la documentación oficial y el libro de cabecera de ruby.

Es posible que a estas expresiones regulares se les pueda hacer alguna optimización, pero por el momento las usaremos tal como están.

Validar el formato

Una primera aproximación es realizar la validación de estos campos por medio del validador de formato que ya incorpora Rails, de tal manera que tendríamos un modelo parecido al siguiente:

class Empleado ActiveRecord::Base
  validates :rfc, format: { with: /\A[A-ZÑ&]{3,4}[0-9]{2}[0-1][0-9][0-3][0-9]([A-Z0-9]{3})?\z/i, message: 'no es un formato de RFC válido' }
  validates :curp, format: { with: /\A[A-Z][AEIOUX][A-Z]{2}[0-9]{2}[0-1][0-9][0-3][0-9][MH][A-Z][BCDFGHJKLMNÑPQRSTVWXYZ]{4}[0-9A-Z][0-9]\z/i, message: 'no es un formato de CURP válido' }
end

Si ejecutamos nuevamente nuestro conjunto de pruebas

rspec spec/models


Finished in 0.05323 seconds
24 examples, 0 failures

Esto significa que todo ha funcionado como esperamos.

Validadores

Ahora bien, si la misma validación la realizamos en más de un modelo (por ejemplo, Empresa, Cliente, Representante, etc.), lo mejor es extraer la funcionalidad en un validador propio con el fin de reutilizar el código. Afortunadamente, Rails nos facilita mucho el trabajo, estableciendo una estructura que nosotros debemos de seguir si queremos agregar validadores. Así que, manos a la obra.

Antes de crear los validadores, debemos decidir en qué directorio colocarlos. Una opción sería agregarlos a la carpeta de modelos, pero al ser una funcionalidad que no es exclusiva de nuestra aplicación sino una biblioteca general, lo correcto sería ubicarlos en la carpeta lib.

Primero, creamos el directorio lib/validators y creamos los archivos rfc_format_validator.rb y curp_format_validator.rb dentro de ese directorio.

El contenido de ambos archivos se lista a continuación

#lib/validators/rfc_format_validator.rb
class RfcFormatValidator < ActiveModel::EachValidator
  def validate_each(object, attribute, value)
    unless value =~ /\A[A-ZÑ&]{3,4}[0-9]{2}[0-1][0-9][0-3][0-9]([A-Z0-9]{3})?\z/i
      object.errors[attribute] << (options[:message] || "no es un RFC válido") 
    end
  end
end



#lib/validators/curp_format_validator.rb
class CurpFormatValidator < ActiveModel::EachValidator
  def validate_each(object, attribute, value)
    unless value =~ /\A[A-Z][AEIOUX][A-Z]{2}[0-9]{2}[0-1][0-9][0-3][0-9][MH][A-Z][BCDFGHJKLMNÑPQRSTVWXYZ]{4}[0-9A-Z][0-9]\z/i
      object.errors[attribute] << (options[:message] || "no es una CURP válida") 
    end
  end
end

Poner nuestro código dentro del directorio lib trae consigo un pequeño problema: a partir de Rails 3, este directorio no se carga automáticamente, así que tendremos que indicarle a Rails que queremos que cargue automáticamente nuestros validadores.

En el archivo config/application.rb, agregamos la siguiente línea

config.autoload_paths += %W(#{Rails.root}/lib/validators)

Si queremos que cargue varios directorios, basta con que los agreguemos a la lista.

Refactor

Y listo, con esto podemos refactorizar el código de nuestro modelo para incluir los validadores que acabamos de crear

class Empleado < ActiveRecord::Base
  validates :rfc, rfc_format: true
  validates :curp, curp_format: true
end

Volvemos a ejecutar nuestras pruebas para asegurarnos que todo funcione correctamente:

rspec spec/models

Finished in 0.05323 seconds
24 examples, 0 failures

Todo funciona perfectamente. Sin embargo, hasta el momento, todo nuestro trabajo lo hemos hecho dentro de nuestra aplicación Rails. ¿Qué sucede si en un nuevo proyecto necesitamos hacer estas validaciones? Una opción sería copiar estos archivos a todos los proyectos que requieran validar CURP y RFC, pero si de pronto encontramos que nuestra expresión regular tiene un error y debemos corregirla, o si encontramos una optimización que hacer, tendríamos que modificar estos archivos en todos los proyectos en los que los estemos utilizando. Una mejor opción sería crear una gema y reutilizar nuestros validadores a través de ella. Y eso es precisamente lo que haremos en la segunda parte de este tutorial.

Azarel Doroteo Pacheco es albañil de software en LogicalBricks.