apux

apux

Publicado
Octubre 15, 2013

Próximos Eventos

Blog

Parte III: Publicación de la gema 'identificamex'

Objetivo

Permitir que la gema identificamex que acabamos de crear, se pueda integrar a cualquier proyecto Rails que quiera hacer uso de ella, agregándola como dependencia a su Gemfile

gem 'identificamex'

Agregar la gema indicando el path

Hasta el momento, nuestro trabajo ha rendido frutos porque ya creamos nuestros validadores y ya tenemos una gema que los integra, pero todavía no hemos utilizado esa gema en un proyecto Rails. Ése es el siguiente paso.

Para esto, retomaremos el proyecto con el que empezamos a trabajar en la parte I de este tutorial, nuestro olvidado proyecto my_app.

Antes de intentar publicar nuestra gema, me gustaría que la probáramos para verificar que todo funcione correctamente. Para esto, vamos a utilizar una funcionalidad bastante útil de ruby gems, que es la posibilidad de indicar el path local donde se encuentra la gema.

En nuestra aplicación Rails my_app, elminamos el directorio lib/validators y eliminamos la línea que cargaba ese directorio, config.autoload_paths += %W(#{Rails.root}/lib/validators). Ahora agregamos la siguiente dependencia nuestro Gemfile:

gem 'identificamex', path: '../identificamex'

El path es la dirección relativa en donde se encuentra el código de nuestra gema.

Instalamos

bundle install

Y ejecutamos nuestras pruebas para verificar que todo funcione correctamente.

rspec spec/models

Finished in 0.02801 seconds
24 examples, 0 failures

Todo funciona correctamente. Sin embargo, me gustaría ver un escenario más complicado. A los validadores de ActiveRecord se les pueden indicar opciones, por ejemplo allow_blank, if, unless, etc. Y nosotros no hemos programado nada de eso en nuestros validadores. Veamos cómo se comporta.

Validadores con opciones

Para tener un panorama más completo, imaginemos que, por alguna razón, nuestra validación se complica un poco. Nuestro cliente nos dice que la CURP del empleado es opcional, y el RFC sólo se debe validar si el empleado es un contribuyente registrado.

Agregamos los ejemplos que prueban ese funcionamiento a nuestro conjunto de pruebas, que queda de la siguiente manera:

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

describe Empleado do

  def empleado_registrado(options)
    # método de utilería para crear empleados registrados
    Empleado.new(options.merge(registrado: true))
  end

  def empleado_no_registrado(options)
    # método de utilería para crear empleados no registrados
    Empleado.new(options.merge(registrado: false))
  end

  describe '#rfc' do
    context 'when registered (contribuyente registrado)' do
      context 'with valid data' do
        it 'accepts rfc (completo)' do
          expect(empleado_registrado(rfc: 'AAAA111111AAA')).to have(:no).errors_on(:rfc)
        end

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

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

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

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

        it 'accepts rfc (datos de nombre con &)' do
          expect(empleado_registrado(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_registrado(rfc: '9AAA111111')).to have(1).error_on(:rfc)
        end

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

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

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

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

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

        it 'refuses rfc (vacío)' do
          expect(empleado_registrado(rfc: '')).to have(1).error_on(:rfc)
        end
      end
    end

    context 'when no registered (contribuyente no registrado)' do
      it 'accepts RFC (válido)' do
        expect(empleado_no_registrado(rfc: 'AAAA111111AAA')).to have(:no).errors_on(:rfc)
      end

      it 'accepts RFC (inválido)' do
        expect(empleado_no_registrado(rfc: 'AA*A112511&XAL;I')).to have(:no).errors_on(:rfc)
      end

      it 'accepts rfc (vacío)' do
        expect(empleado_no_registrado(rfc: '')).to have(:no).errors_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

      it 'accepts curp as blank' do
        expect(Empleado.new(curp: '')).to have(:no).error_on(:curp)
      end

      it 'accepts curp as nil' do
        expect(Empleado.new(curp: nil)).to have(:no).error_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

Lo que hicimos fue modificar los ejemplos anteriores que verificaban el comportamiento del RFC para contemplar el nuevo comportamiento que depende depende de que el empleado sea un contribuyente registrado o no. Eso nos llevó a separar los contextos: cuando es un contribuyente registado, las validaciones se mantienen, y cuando no es un contribuyente registrado, no importa qué valor se asigne al RFC, siempre será aceptado. Para crear esos objectos, hicimos uso de unos métodos de utilería empleado_registrado y empleado_no_registrado, que en un proyecto real se encargarían de generar empleados registrados y no registrados según complejas reglas de negocio, pero en nuestro caso sólo ponen la variable registrado en true o false según corresponda. También se agregaron ejemplos que prueban que se acepta la CURP vacía.

Es cierto que el código de nuestras pruebas no es el óptimo, se puede refactorizar para mejorarlo. Una buena alternativa sería usar factories para generar nuestros modelos en lugar de nuestros métodos de utilería, pero es un tema completo que nos desviaría mucho de nuestra tarea, por lo que lo mantendremos de esa manera porque creo que para fines del tutorial es suficientemente claro.

El código de nuestra clase `Empleado` quedaría como sigue:


class Empleado < ActiveRecord::Base
  attr_accessor :registrado

  validates :rfc, rfc_format: true, if: :contribuyente_registrado?
  validates :curp, curp_format: true, allow_blank: true

  def contribuyente_registrado?
    registrado
  end

end

De nuevo, el método contribuyente_registrado? es una simplificación del código real (que seguramente involucraría reglas de negocio complejas), pero funciona para probar nuestro ejemplo. Ejecutemos las pruebas para verificar que funcionen correctamente.

rspec spec/models

Finished in 0.05310 seconds
30 examples, 0 failures

Como vemos, nuestros validadores se integran sin ningún problema con las opciones de ActiveRecord que estamos acostumbrados a utilizar, y sin que tengamos que modificar el código, por lo que podemos decir que nuestra gema está completa. Ahora veremos cómo publicarla para que cualquier persona la pueda integrar en su proyecto.

Git y Github

Para esto, crearemos un proyecto en github. Si necesitas más información sobre cómo crear repositorios en github, revisa este tutorial.

Si no tienes configurado git aún, debes indicar tu nombre y correo electrónico

git config --global user.email "you@example.com"
git config --global user.name "Your Name"

Si sólo deseas configurar esos datos para este proyecto y no para todo tu entorno, no utilices la opcion --global.

Si estás utilizando git 1.8 o superior, se recomienda usar la siguiente configuración:

git config --global push.default simple

Ya que será el comportamiento por omisión para git 2.0. Esta opción le indica a git que cuando se haga push trabajará sólo con la rama actual, y no con todo el repositorio, como lo hacía con versiones anteriores.

Ahora sí, con git configurado, debemos agregar nuestros archivos a nuestro controlador de versiones. Aquí debemos indicar qué archivos compondrán parte de nuestra gema. En nuestro caso, vamos a incluir todos los archivos.

git add .

hacemos commit de los archivos

git commit -m "Primera versión de la gema."

y hacemos push de nuestro proyecto

git push origin master

Con esto, hemos conseguido un gran avance: cualquier usuario puede usar nuestra gema clonando nuestro proyecto en github y agregando la ruta en el Gemfile de su aplicación. Pero no es exactamente el mejor flujo para integrar gemas a un proyecto. Lo ideal es que simplemente agregue la gema como dependencia y rubygems se encargue de todo lo demás, sin que el usuario tenga que descargar manualmente el código. Veamos cómo podemos hacer esto.

Publicar en RubyGems

Algo que hasta el momento habíamos pasado por alto es la documentación de nuestra gema. Si la estamos compartiendo con el mundo, necesitamos indicar cómo debe ser utilizada. Dado que nuestra gema tiene una funcionalidad muy básica, no es mucho lo que tenemos que documentar por ahora, basta con explicar para qué sirve y cómo utilizarla. Para esto, nos basta el archivo de README.

Bundler nos generó una estructura bastante completa para este archivo, por lo que a nosotros nos corresponde sólo introducir la información adecuada.

Con la documentación de nuestra gema completa, podemos publicarla en RubyGems. Para esto, lo primero que tenemos que hacer es registrarnos en rubygems

Ahora sí, podemos publicar nuestra gema.

rake release

Este comando realiza dos tareas: crear un tag de git con la versión actual de nuestra gema y publicar esa versión en rubygems. Aquí es donde se vuelve importante actualizar la versión de nuestra gema cada que la liberemos, ya que de lo contrario obtendríamos un error al tratar de liberar dos veces la misma versión de la gema.

Si es la primera vez que trabajamos con rubygems, es probable que nos encontremos con este error:

rake aborted!
Your rubygems.org credentials aren't set. Run `gem push` to set them.

Si esto sucede, basta con seguir la instrucción que nos indica el mensaje de error, introducir nuestras credenciales (email y password) y... ¡listo!. Con eso podemos publicar nuestra gema cada vez con rake release.

Integrar la gema con la aplicación

A estas alturas, ya no hay mucho trabajo por hacer, lo único que nos falta es delegar completamente la carga de nuestra gema a rubygems, sin que tengamos que indicarle el directorio. Como nuestra gema ya está publicada, no nos queda más que eliminar el path en nuestro Gemfile. La línea quedaría de la siguiente manera:

gem 'identificamex'

Para asegurarnos que estamos tomando la versión publicada, debemos actualizar nuestra gema con bunlder.

bundle update identificamex

Por último, volvemos a ejecutar nuestras pruebas:

rspec spec/models

Finished in 0.05518 seconds
30 examples, 0 failures

Todo sigue funcionando, así que podemos dar por finalizada nuestra tarea.

Si te interesa revisar el código real de la gema, puedes verlo en github, sólo toma en cuenta que la gema ha crecido un poco e incluye más funcionalidad de la que se muestra en el tutorial. Además, en la gema utilicé MiniTest en lugar de RSpec, pero el principio es el mismo.

Cualquier corrección a este tutorial o a la gema, será bienvenida.

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