programación y pataletas

Hola, soy Federico Builes, programador y predicador barato de Ruby.
Programo en GitHub y peleo en Twitter.
Nov 19
Permalink

Cómo Escribir un Plugin para Rails

Donde intento demostrar como escribir un plugin para Ruby on Rails. En este caso, usaré zodiac como ejemplo. Zodiac es un helper que retorna el signo zodiacal de una fecha dada:

 zodiac_sign_for(Time.utc(2000, "jun", 12))
 # => "Gemini"     

Generando un esqueleto

Necesitamos crear una aplicación de Rails para ejecutar algunos generadores:

 $ rails ejemplo
 $ cd ejemplo

Ahora, es hora de usar el generador estándar de Rails para crear el esqueleto del plugin:

 $ script/generate plugin zodiac
       create  vendor/plugins/zodiac/lib
       create  vendor/plugins/zodiac/tasks
       create  vendor/plugins/zodiac/test
       create  vendor/plugins/zodiac/README
       create  vendor/plugins/zodiac/MIT-LICENSE
       create  vendor/plugins/zodiac/Rakefile
       create  vendor/plugins/zodiac/init.rb
       create  vendor/plugins/zodiac/install.rb
       create  vendor/plugins/zodiac/uninstall.rb
       create  vendor/plugins/zodiac/lib/zodiac.rb
       create  vendor/plugins/zodiac/tasks/zodiac_tasks.rake
       create  vendor/plugins/zodiac/test/zodiac_test.rb

Desde ahora podemos trabajar dentro de esta misma aplicación o podemos mover el plugin afuera y trabajar sobre el. Inicialmente el generador crea los siguientes archivos/directorios:

lib: Aquí es donde ira todo nuestro código.

tasks: Tareas que Rake podrá ejecutar, por ejemplo, mover archivos o ejecutar un set de RSpec.

test: Directorio para nuestros tests.

README: Un README por defecto que podemos usar para explicar el funcionamiento de nuestro plugin.

MIT-LICENSE: El generador crea por defecto un archivo para la licencia del plugin usando la misma licencia de Rails (MIT). Nada nos impide cambiarlo para usar nuestra licencia preferida.

Rakefile: Un archivo de Rake con unas tareas bastante básicas: Correr los tests y generar la documentación RDoc.

init.rb: El “punto de enganche” de nuestra aplicación. Rails ejecutará este archivo cada vez que sea cargado, así que aquí hacemos las inclusiones/”mixins” necesarios.

install.rb/uninstall.rb: Estos scripts serán ejecutados a la hora de instalar/desinstalar nuestro plugin. Podemos incluir aquí migraciones o copias de archivos necesarias para la instalación del plugin.

No es necesario que usemos todos estos archivos/directorios para cada plugin que hagamos, así que podemos eliminar tranquilamente lo que no usaremos. En el caso de Zodiac, no necesitamos tareas adicionales para Rake así que podemos deshacernos del directorio tasks/. Si pensamos usar RSpec en vez de Test/Unit podemos eliminar también el directorio test/ y crear un directorio spec/. No haremos ninguna modificación a la hora de instalar/desinstalar el plugin así que podemos olvidarlos de install.rb/uninstall.rb

Primeros Pasos

Empezaremos por llenar nuestro archivo README con información útil sobre nuestro plugin:

 Zodiac
 ========
 A Rails plugin to get the zodiac sign on a given date.

 Example
 -------
   zodiac_sign_for(Time.utc(2000, "jun", 12))
   # => "Gemini"

 Copyright (c) 2008 Federico Builes, released under the MIT license.
 Fork freely!     

Además, intentaremos seguir un acercamiento TDD, así que escribamos algunos tests. Por gusto personal prefiero usar RSpec a Test/Unit, así que me he olvidado del directorio test/, he creado un directorio specs/ y dentro de este tengo tres archivos:

spec_helper.rb
Este es el helper que se encargara de ejecutar las especificaciones cuando hagamos uso de Rake:

 require File.dirname(__FILE__) + "/../lib/zodiac_helper"

spec.opts
Algunas opciones para modificar la salida de RSpec:

 --colour
 --format
 specdoc
 --loadby
 mtime

zodiac_helper_spec.rb
La especificación de nuestro helper:

Finalmente, he añadido una pequeña tarea a Rakefile para ejecutar las specs:

Rakefile

 desc "Runs the spec suite"
 task :spec do
   spec_path = "#{File.dirname(__FILE__)}/spec"
   sh "spec #{spec_path} -O #{spec_path}/spec.opts "
 end

Perfecto, podemos correr nuestras specs con rake spec y veremos 4 errores, perfecto para empezar.

Escribiendo el código

Es hora de programar nuestro pequeño helper. Ya que nuestro plugin es bastante simple, podemos insertarlo todo en lib/zodiac.rb

Para empezar, queremos incluir todo nuestro trabajo dentro de un modulo para el plugin, en este caso, ZodiacHelper:

 module ZodiacHelper
 end

La idea de escribir todo dentro de un modulo es poder hacer un mixin con los módulos de Rails cuando estemos cargando el plugin. Rails está divido en varias partes, así que solo queremos hacer la inclusión con ciertos módulos de nuestra aplicación. Un mixin nos permite incluir nuestros módulos dentro de los módulos de Rails, así que terminamos añadiendo nuestro código al código de uno (o varios) subsistemas de nuestro framework.

Si estuviésemos escribiendo un plugin para modificar modelos podríamos hace la inclusión dentro de ActiveRecord::Base mientras que si estamos escribiendo un par de helpers para las vistas haremos entonces la inclusión en ActionView::Base.

Nuestro plugin tiene un único método, zodiac_sign_for, que se encarga de recibir una fecha (como un Time o como un entero) y devolver el signo zodiacal que corresponde a este día. Lo que haremos entonces es :

  1. Recibir una fecha. Si esta fecha responde al método strftime entonces lo convertiremos a un formato númerico compuesto por mes-día:

    Octubre 12 => 1012
    Julio 7 => 707

Si la fecha dada no responde a este método entonces supondremos que ya está en este formato.

Vale la pena resaltar que el ejemplo Julio 7 se resume a 707 y no 0707. Esto es porque Ruby utiliza el prefijo 0 para referirse a números en base octal, así que 0708 es realmente 455 en base 10.

  1. Teniendo esta combinación de mes/día usaremos una expresión case para saber que signo zodiacal corresponde a la fecha dada. El rango de número entonces va desde 11 (Enero 1 — que también puede ser representado como 101) hasta 1231 (Diciembre 31). Cualquier número menor a 11 o por encima de 1231 resultará entonces en error.
    Además de esto, debemos comprobar que los rangos para los días siempre estén entre 1 y 31, ningún mes tiene más días.

  2. Si el número está entre los rangos mencionados en el paso anterior devolveremos una cadena con el signo zodiacal. Sino, lanzaremos una excepción con una pequeña descripción del problema.

zodiac_helper.rb

Añadimos además un comentario explicando un poco el funcionamiento de nuestro método.

Probando, probando

Teniendo listo nuestro código, es hora de probarlo con las especificaciones que escribimos previamente:

 $ rake spec

 Zodiac
 - returns the zodiac sign for a given Time
 - returns the zodiac sign for a given number
 - receives a number that represents a month and day
 - raises if the date's invalid

 Finished in 0.045702 seconds

 4 examples, 0 failures

Excelente, todo parece estar funcionando, podemos pasar a insertarlo dentro de Rails.

ee-neat

Previamente mencionamos que init.rb es el punto de enganche de nuestro plugin con Rails. Es aquí donde debemos crear el mixin de nuestro modulo con los módulos del framework. Para el caso de Zodiac queremos asegurarnos de que el programador pueda usarlo tanto en sus vistas (tal vez para mostrar la información de un perfil en su próxima red social de coleccionistas de estampillas de Sri Lanka) como en su modelo (en caso de que simplemente quiera guardarlo en la base de datos y vender toda esta información al futuro Google Astrologer). Así pues, necesitamos incluir nuestro modulo en dos lugares diferentes:

  1. ActionView::Base — que se encargara de hacer disponible el método a nuestras vistas.
  2. ActiveRecord::Base — para poder usarlo dentro de nuestros modelos.

Incluir nuestro módulo en estos dos es tan fácil como:

init.rb

 ActionView::Base.send :include, ZodiacHelper
 ActiveRecord::Base.send :include, ZodiacHelper

Trivial. De ahora en adelante podemos usar nuestro modulo en cualquiera de estos dos lugares sin necesidad de hacer nada mágico.

¿Qué pasa si nos saltamos este paso? Rails no tendrá manera de saber a que nos referimos cuando le pidamos el método zodiac_sign_for así que tendremos que incluir a mano ZodiacHelper (include ZodiacHelper será suficiente) en cada lugar que queramos usarlo. Por comodidad haremos el enganche desde init.rb como mencionamos anteriormente.

Only wimps use tape backup: real men just upload their important stuff on ftp, and let the rest of the world mirror it.

Finalmente, nuestro plugin no será de utilidad a menos de que podamos distribuirlo a otros programadores. En este caso lo único que debemos hacer es subir nuestro plugin a nuestro repositorio de código fuente favorito (o sea, GitHub, y este punto no es discutible).

Luego de tener el plugin (y únicamente el plugin, no toda la aplicación de Rails) en nuestro repositorio, podremos instalarlo desde otras máquinas o aplicaciones así:

 $ script/plugin install git://github.com/febuiles/zodiac.git

Reemplazando la dirección web usada por nuestro link para clone en GitHub podremos distribuir el plugin a todo el mundo y hacernos millonarios prediciendo el zodiaco.

Postroducción

Algunas ideas de lo que podemos hacer después de haber publicado nuestro plugin:

  • install.rb es un excelente punto para incluir la documentación inicial que será mostrada al usuario cuando instale el plugin por primera vez, es usual imprimir el README en este script.
  • Ruby on Rails Plugins es tal vez el repositorio más grande de plugins para Rails, podemos añadir nuestro plugin a este sitio web.
  • Working With Rails Es un buen sitio para autopromoción y listar nuestro plugin en el perfil personal hará subir un poco el “rating profesional”.

De salida

El código fuente completo de este mini-proyecto se puede encontrar en: http://github.com/febuiles/zodiac.

Para cualquier correción, insulto por mala gramática, pregunta, o sugerencia, federico.builes@gmail.com