apux

apux

Publicado
Octubre 1, 2013

Próximos Eventos

Blog

Parte II: Creación de una gema que incorpore los validadores de CURP y RFC

Objetivo

Crear una gema que incluya los validadores de CURP y RFC que se generaron en la Parte I de este tutorial.

Creación de la gema

Una de las tareas más difíciles al momento de crear una gema es escoger un nombre. Al menos, es una de las más complicadas para mí. Para simplificar, llamémosle identificamex.

Lo primero que debemos hacer es crear la gema. Existen varias formas de hacer esto, una de las más fáciles es utilizar bundler.

bundle gem identificamex

Y listo. Bundler se encargó de crear toda la estructura necesaria de una gema genérica. Ahora sólo tenemos que agregar el contenido propio de la nuestra.

Los archivos que incluye la gema son el Gemfile, el Rakefile, el README, la licencia y un archivo .gemspec. Además, creó un directorio llamado lib y dentro de ese directorio un archivo llamado identificamex.rb, que define un módulo vacío. También creó otro archivo, lib/identificamex/version.rb, que tiene la versión de la gema (por omisión, '0.0.1').

De todos estos archivos, el que más nos intriga es identificamex.gemspec, de los demás se puede inferir fácilmente su utilidad.

Gemspec

El archivo identificamex.gemspec es el archivo básico de configuración para nuestra gema. Cada gema tiene un archivo con el nombre de la gema y la extensión .gemspec. En este archivo se configura el nombre, la descripción y la versión de la gema, los archivos que la componen, datos de los autores, etc.

Nota cómo hay un pequeño truco para indicar qué archivos conforman la gema. En lugar de listarlos todos, se hace uso de git. Es decir, la gema se compondrá de los archivos que nosotros agreguemos a la estructura de git. Ojo, esto significa que si nosotros creamos un archivo nuevo, y no lo agregamos a git, nuestras pruebas locales pueden funcionar correctamente, pero al liberar la gema ésta fallará porque le harán falta archivos.

Si no conoces git, puedes empezar revisando el sitio oficial (hay una sección de tutoriales), y practicando en el sitio interactivo.

Otro detalle a tomar en cuenta es que la versión de la gema no se especifica directamente en el archivo (aunque puede hacerse). En lugar de eso, se hace referencia al valor de una variable: Identificamex::VERSION. Esa variable la podremos encontrar en el archivo lib/identificamex/version.rb que se creó automáticamente al crear la gema. Cada nueva versión de nuestra gema, habremos de cambiar el valor de esa variable.

El resto de parámetros de configuración son bastante intuitivos.

Dependecias

Es probable que nuestra gema dependa de que existan otras gemas para que funcione correctamente, de ser así, debemos especificar esas dependencias. Al igual que un proyecto Rails, nuestra gema tiene un archivo Gemfile, donde podríamos agregar las gemas de las que depende, sin embargo, en el caso de las gemas, hay un mejor archivo para hacer esto, y ese archivo es identificamex.gemspec. Este es el archivo general para la configuración de nuestra gema. Y en nuestro caso, agregaremos también la dependencia de ActiveModel, ya que esta gema es necesaria para que nuestros validadores funcionen.

Al momento, nuestro archivo gemspec contiene lo siguiente.

# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'identificamex/version'

Gem::Specification.new do |spec|
    spec.name          = "identificamex"
    spec.version       = Identificamex::VERSION
    spec.authors       = ["Azarel Doroteo Pacheco"]
    spec.email         = ["azarel.doroteo@logicalbricks.com"]
    spec.description   = %q{Validadores para los formatos de CURP y RFC}
    spec.summary       = %q{Validadores sencillos para los formatos de la Clave Única de Registro de Población (CURP) y el Registro Federal de Contribuyentes (RFC) utilizados en México}
    spec.homepage      = "https://github.com/LogicalBricks/identificamex"
    spec.license       = "MIT"

    spec.files         = `git ls-files`.split($/)
    spec.executables   = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
    spec.test_files    = spec.files.grep(%r{^(test|spec|features)/})
    spec.require_paths = ["lib"]

    spec.add_development_dependency "bundler", "~> 1.3"
    spec.add_development_dependency "rake"

    spec.add_dependency 'activemodel'
end

Dependencias de desarrollo

Ahora bien, existen dependencias que sólo afectan al momento del desarrollo pero no a la funcionalidad de la gema en sí. Por ejemplo, las bibliotecas de pruebas (en nuestro caso RSpec). Si agregamos rspec como dependencia, se instalará automáticamente cuando se instale nuestra gema y no queremos eso. Esta dependencia sólo afecta si alguien descarga nuestro código con la intensión de modificarlo, en cuyo caso sí se debe instalar la biblioteca de pruebas; pero si un usuario sólo quiere instalar nuestra gema para usarla y no para modificarla, la biblioteca de pruebas no debe considerarse dependencia. Es el mismo caso que presenta bundler y rake.

Para estos casos, la dependencia se debe especificar de la siguiente manera:

spec.add_development_dependency 'rspec'

Y por último instalamos las dependencias con bundler:

bundle install

Validadores

Como ya creamos nuestros validadores para nuestra aplicación Rails, nos tomamos la libertad de copiarlos al directorio lib. Siendo estrictos con TDD, primero deberíamos realizar las pruebas y luego programar la funcionalidad, pero en este caso, como la funcionalidad ya la tenemos, podemos tomarnos ese permiso.

#lib/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/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

Pruebas

De nuevo, es necesario agregar pruebas a nuestra biblioteca para asegurarnos que todo esté funcionando como debe y que las futuras modificaciones no rompan nada. Gracias a nuestro conjunto de pruebas que creamos en la primera parte de este tutorial, esta tarea se simplifica un poco. Desafortunadamente, la transición no será tan sencilla, tendremos que sortear algunos problemas antes de conseguir que las pruebas funcionen adecuadamente.

Lo primero que hay que tomar en cuenta es que la funcionalidad que debemos de probar no es una funcionalidad aislada, sino una funcionalidad que se agrega a un modelo de Rails. Por lo tanto, necesitaremos crear un modelo Rails para poder probar nuestros validadores.

¿Significa que dependemos de que Rails esté instalado para que podamos probar nuestra gema? ¿Tenemos que agregar la dependencia a 'rails' en lugar de 'activemodel'? ¿Debemos agregar 'rails' como dependencia de desarrollo? Afortunadamente, la respuesta a todas estas preguntas es 'No'. No necesitamos en lo absoluto de Rails en nuestra gema. Rails agrega la funcionalidad de validadores mediante la gema de ActiveModel, que nosotros ya tenemos instalada.

Ahora bien, si revisamos el código de nuestra aplicación, veremos que nuestra clase Empleado hereda de ActiveRecord::Base. Pero, de nuevo, nosotros no tenemos accesso a ActiveRecord, sólo a ActiveModel. Podríamos agregar la dependencia a ActiveRecord directamente, en lugar de ActiveModel, pero no sería lo adecuado.

ActiveRecord vs ActiveModel

¿Qué es exactamente lo que hace ActiveRecord y qué es lo que hace ActiveModel? La funcionalidad de ambas bibliotecas puede confundirnos un poco. No es la intensión de este tutorial estudiar a fondo el comportamiento de estas herramientas, pero sí necesitamos conocer los aspectos básicos que nos permitan seguir avanzando en la creación de nuestra gema.

Anteriormente, la funcionalidad de los modelos de Rails recaía por completo en ActiveRecord, pero a partir de Rails 3, esta funcionalidad se agregó un nivel más de abstracción con ActiveModel. ActiveRecord realiza muchas tareas muy importantes, como mapear nuestro modelo a una base de datos, realizar las consultas y actualización, validar las entradas, etc. Sólo que ahora, las tareas que no corresponden a la base de datos son delegadas a ActiveModel. De esta manera, podemos tener un modelo con la funcionalidad completa de un modelo de Rails, pero sin la parte de la base de datos. Esto nos beneficia mucho, ya que para el desarrollo de nuestra gema, no necesitamos que los valores sean persistentes: sólo necesitamos que se puedan validar y saber si hay errores o no.

Nuestro instinto nos podría indicar que si no queremos la funcionalidad de ActiveRecord, y sólo queremos quedarnos con ActiveModel, podríamos crear nuestra clase de la siguiente manera:

class Empleado < ActiveModel::Base
end

Desafortunadamente, esto no funciona porque no existe una clase Base en el módulo ActiveModel. En estos casos, lo que se necesita hacer es agregar la funcionalidad como un módulo. El módulo completo es ActiveModel::Model, pero tampoco necesitamos del módulo completo, para nuestra gema es suficiente trabajar con ActiveModel::Validations. Así, nuestra clase queda de la siguiente manera:

class Empleado
   include ActiveModel::Validations
end

Y ahora sí, tenemos nuestro modelo que permite agregar validaciones.

Regresemos entonces a nuestras pruebas. Para empezar, necesitamos crear un archivo spec/spec_helper.rb. Este archivo es similar al que la gema de rspec-rails creó por nosotros en nuestra aplicación, pero para nuestra gema, vamos a crear una versión mucho más básica:

# spec/spec_helper.rb
require 'identificamex'

Lo único que incluye el spec_helper es una línea que requiere el archivo importador. Este archivo es el que se creó al inicio de este proceso, con el módulo vacío. Modificamos ese archivo para incluir los archivos de nuestros validadores.

require 'active_model'
require "identificamex/version"
require "curp_format_validator"
require "rfc_format_validator"

module Identificamex
end

Specs

Ahora sí, ya podemos programar nuestros specs. Nuestro primer intento, como buenos desarrolladores, será reutilizar las mismas pruebas que teníamos en nuestra aplicación. Desafortunadamente eso no será posible, ya que hay algunos problemas que necesitamos solucionar. Primero, las pruebas en la aplicación son bastante elegantes, con sintaxis de este tipo:

expect(Empleado.new(rfc: 'AAAA111111AAA')).to have(:no).errors_on(:rfc)
expect(Empleado.new(curp: 'AAAA111111KDFBBB01')).to have(1).error_on(:curp)

Pero esto no funciona dentro de nuestra gema, porque los métodos errors_on y error_on no existen en rspec: son agregados por la gema rspec-rails, que no tenemos en nuestra gema. Por lo tanto, tenemos que cambiar un poco la forma de los specs.

La idea sería escribir nuestros specs de la siguiente manera:

expect(Empleado.new(rfc: 'AAAA111111AAA')).to be_valid
expect(Empleado.new(curp: 'AAAA111111KDFBBB01')).to_not be_valid

La clase Empleado

Para esto, tenemos que modificar la clase Empleado que creamos anteriormente para que acepte parámetros. Además, para asegurarnos que el modelo sea válido por omisión, en caso de no recibir un parámetro, se inicializará con un valor válido.

class Empleado
  include ActiveModel::Validations
  attr_accessor :curp, :rfc
  validates :curp, curp_format: true
  validates :rfc, rfc_format: true

  def initialize(options)
    @curp = options[:curp] || 'AAAA111111HDFBBB01'
    @rfc  = options[:rfc]  || 'AAAA111111AAA'
  end
end

Como no dependemos de una base de datos, los campos se han agregado como accessors a las varibales curp y rfc, se han agregado las validaciones de formato a esas variables, y se ha agregado un método initialize que inicializa las variables correctamente.

El archivo de specs completo queda de la siguiente manera:

#spec/validators_spec.rb

require 'spec_helper'

class Empleado
  include ActiveModel::Validations
  attr_accessor :curp, :rfc
  validates :curp, curp_format: true
  validates :rfc, rfc_format: true

  def initialize(options)
    @curp = options[:curp] || 'AAAA111111HDFBBB01'
    @rfc  = options[:rfc]  || 'AAAA111111AAA'
  end
end

describe 'rfc validator' do

  context 'with valid data' do
    it 'accepts rfc (completo)' do
      expect(Empleado.new(rfc: 'AAAA111111AAA')).to be_valid
    end

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

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

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

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

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

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

    it 'refuses rfc (caracter invalido)' do
      expect(Empleado.new(rfc: 'A*AA111111')).to_not be_valid
    end

    it 'refuses rfc (falta un digito en la fecha)' do
      expect(Empleado.new(rfc: 'AAAA11111')).to_not be_valid
    end

    it 'refuses rfc (falta un caracter en los datos del nombre)' do
      expect(Empleado.new(rfc: 'AA111111')).to_not be_valid
    end

    it 'refuses rfc (el dia es invalido 42)' do
      expect(Empleado.new(rfc: 'AAAA111142')).to_not be_valid
    end

    it 'refuses rfc (el mes es invaido 25)' do
      expect(Empleado.new(rfc: 'AAAA112511')).to_not be_valid
    end
  end

end

describe 'curp validator' do

  context 'with valid data' do
    it 'accepts curp (hombre)' do
      expect(Empleado.new(curp: 'AAAA111111HDFBBB01')).to be_valid
    end

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

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

  end

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

    it 'refuses curp (caracter invalido)' do
      expect(Empleado.new(curp: 'A*AA111111HDFBBB01')).to_not be_valid
    end

    it 'refuses curp (falta un digito en la fecha)' do
      expect(Empleado.new(curp: 'AAAA11111HDFBBB01')).to_not be_valid
    end

    it 'refuses curp (falta un caracter en los datos del nombre)' do
      expect(Empleado.new(curp: 'AAA111111HDFBBB01')).to_not be_valid
    end

    it 'refuses curp (el dia es invalido 42)' do
      expect(Empleado.new(curp: 'AAAA111142HDFBBB01')).to_not be_valid
    end

    it 'refuses curp (el mes es invaido 25)' do
      expect(Empleado.new(curp: 'AAAA112511HDFBBB01')).to_not be_valid
    end

    it 'refuses curp (sexo es inválido K)' do
      expect(Empleado.new(curp: 'AAAA111111KDFBBB01')).to_not be_valid
    end

    it 'refuses curp (caracteres de consonantes tiene alguna vocal)' do
      expect(Empleado.new(curp: 'AAAA111111HDFABB01')).to_not be_valid
      expect(Empleado.new(curp: 'AAAA111111HDFBAB01')).to_not be_valid
      expect(Empleado.new(curp: 'AAAA111111HDFBBA01')).to_not be_valid
    end

    it 'refuses curp (digito verificador alfanumérico)' do
      expect(Empleado.new(curp: 'AAAA111111HDFBBB0A')).to_not be_valid
    end

  end

end

Es básicamente el mismo conjunto de pruebas de nuestra aplicación pero adaptado para nuestra gema. Por supuesto, sería mejor que las pruebas estuvieran en archivos separados y el modelo auxiliar también, pero para fines del tutorial, podemos trabajar con ese archivo.

Si ejecutamos nuestro conjunto de pruebas:

rspec .

Finished in 0.00984 seconds
24 examples, 0 failures

Como podemos ver, todo ha salido de maravilla. Ahora tenemos una gema que agrega los validadores. Sin embargo, aún nos falta publicar nuestra gema para que se pueda utilizar en otros proyectos Rails. De eso trata la última parte de este tutorial.

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