Creando un pequeño DSL en Ruby
Estaba aprovechando este fin de semana algo largo debido a la semana santa y recorde que tenia que revisar un tutorial muy importante que habia dado Chad Fowler y Marcel Molina Jr en el evento de Scotland On Rails sobre algunas cosas basicas y avanzadas que todo desarrollador en Ruby deberia manejar.
El primer tema que tocaron en el tutorial fueron sobre los Blocks y Closures, este es un punto muy fuerte para mi del lenguaje y junto al famoso method_missing permite que joyas como Rails salgan a la luz, bien aqui mi primer intento para resolver el ejercicio:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
class Configuration def initialize @options = {} end def method_missing(name, *args, &block) if block_given? @options[name.to_s] = Configuration.new block.call(@options[name.to_s]) else if name.to_s =~ /=\Z/ @options[name.to_s.gsub!(/=/,'')] = args.shift else @options[name.to_s] end end end end def configure conf = Configuration.new yield conf conf end # Aqui un ejemplo configuration = configure do |config| config.tail_logs = true config.max_connections = 55 config.app_server do |app_server_config| app_server_config.port = 8808 end end configuration.class # => Configuration configuration.tail_logs # => true configuration.app_server.port # => 8808 |
Para esta version inicial decidí manejar las opciones de configuración en un Hash, basicamente lo que hago es crear una manera distinta de acceder a un Hash, de tal manera que pareciera que estamos accediendo a atributos de un objeto y no a los valores de un Hash.
Lo que uso en la solución es basicamente el poder de method_missing, si recibo un metodo que no existe para el objeto y este no tiene asociado un bloque, entonces sé que tengo que generar un nuevo valor en el Hash, por el contrario si recibo un metodo que no existe pero ademas este tiene asociado un bloque con el, entonces se que tengo que generar un nuevo valor en el Hash, pero este valor tiene que ser una nueva instancia de Configuration y tengo que pasar este objeto al bloque.
Para un segundo planteamiento del problema me acordé de que Ruby trae una libreria que permite hacer lo que necesito: trabajar con un Hash como si se tratara de atributos de un objeto, esta libreria se llama OpenStruct
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
require 'ostruct' class Configuration def initialize @options = OpenStruct.new end def method_missing(name, *args, &block) if block_given? @options.send((name.to_s + '=').to_sym, Configuration.new) block.call(@options.send(name)) else @options.send(name, args.shift) end end end def configure conf = Configuration.new yield conf conf end |
Con esto he ahorrado un par de lineas ;), pero lo que era mas importante era que tenia que aprender sobre esa libreria, ahora comprendo porque Ruby es el lenguaje perfecto para crear un DSL, aqui les dejo también un buen articulo que me ayudo en el tema: http://www.daniel-azuma.com/blog/view/z3bqa0t01uugg1/implementing_dsl_blocks
Happy hacking!
Mi presentación en el Barcamplima
En unos momentos me tocara exponer en el Barcamplima, hasta el momento todo va saliendo muy bien, pueden ver mi diapositiva si desean y espero que sigan viniendo mas eventos de este tipo!
Internacionalización en Rails: Gettext o I18n?
El dia de ayer, alguien hizo esta consulta en la lista de rails hispana en la cual participo y me sirvio bastante par tomar la decisión: Gettext.
Ahora tengo bien en claro que usare Gettext para la traducción de cadenas en mis templates y I18n para para la localización de las fechas y mensajes de error de ActiveRecord
Javascript no intrusivo con Jquery en Rails
He estado tratando de mejorar esta semana mi jquery-fu y una de las tareas que me propuse fue hacer que ciertas acciones comunes en mis controladores como el destroy y el create mejoraran la experiencia del usuario aprovechandose de Ajax y a la vez se porten bien con aquellos usuario que tienen Javascript desactivado, pues bien a la marcha, empezaremos con la acción ‘destroy’:
Cargando librerias de javascript
Lo primero es cargar las librerias javascripts que necesitemos, como estoy usando HAML, lo hago de la siguiente manera en mi layout:
1 |
= javascript_include_tag 'jquery-1.3.2.min', 'jquery.livequery', 'jquery-ui-1.7.custom.min', 'application' |
Creando nueva accion y vista para destroy
He tomado como ejemplo un controlador para el manejo de categorias
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class CategoriesController < ApplicationController def destroy Category.destroy(params[:id]) respond_to do |format| format.html do flash[:notice] = 'Categoria eliminada con exito' redirect_to categories_url end format.js { head :ok } end end def destroy_nojs @category = Category.find(params[:id]) end end |
y la vista quedaria asi:
1 2 3 4 5 6 7 |
-# -#app/views/categories/destroy_nojs.html.haml
%h2== Eliminar categoria: #{@category.name.humanize}
¿Estas seguro que deseas eliminar esta categoria?
- form_for @category, :html => { :method => :delete } do |f|
= f.submit 'Si, eliminar esta categoria'
|
Hasta aqui ya tendremos la accion destroy no intrusiva a diferencia de lo que nos genera esto:
1 |
= link_to 'Eliminar', category_path(category), :method => :delete, :confirm => '¿Seguro ...?' |
Lo de arriba genera codigo Javascript intrusivo.
Ajaxificando la accion delete
Aqui viene la parte buena, pues necesitamos que eliminar una categoria sea una tarea al vuelo y si nos ponemos a pensar bien este puede ser un requerimiento que se repita en muchas partes de nuestra aplicación asi que necesitamos algo que sea reutilizable, pues bien, manos a la obra:
Suponiendo que tenemos una vista index para las categorias algo asi:
1 2 3 4 5 6 7 |
-#app/views/categories/index.html.haml
- for category in @categories
%li
== #{category.name}
= link_to 'Editar', edit_category_path(category)
= link_to 'Eliminar', {:action => 'destroy_nojs', :id => category}, :class => 'delete_category'
|
Entonces nuestro archivo public/javascripts/application.js quedaria algo asi:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$.fn.delete_link = function(msg_confirm, message){
$(this).livequery("click", function() {
if (confirm(msg_confirm)) {
var ajaxTarget = $(this).attr("href").replace(/destroy_nojs\//,"") + ".js";
$.post(ajaxTarget, { _method: "delete" },
function(data){
alert(message);
});
$(this).parent().fadeOut("slow");
}
return false;
});
};
$(document).ready(function(){
$(".delete_ele").delete_link("Seguro que desea eliminar esta categoria?", "Categoria eliminada con éxito");
});
|
Como ven en el código de la funcion delete_link me he creado una especie de convención para los enlaces que son para eliminar recursos y la convención es que deben apuntar a la accion destroy_nojs la cual es la versión no intrusiva, de esta manera puedo crearme una función la cual puedo reutilizarla.
Ajaxificando el formulario de nueva categoria
Esto no podia ser mas sencillo, suponiendo que tenemos en el controlador de categorias lo siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class CategoriesController < ApplicationController def new @category = Category.new @categories = Category.all end def destroy Category.destroy(params[:id]) respond_to do |format| format.html do flash[:notice] = 'Categoria eliminada con exito' redirect_to categories_url end format.js { head :ok } end end def destroy_nojs @category = Category.find(params[:id]) end end |
la vista para la acción new quedaria algo asi:
1 2 3 4 5 6 7 8 9 10 |
-#app/views/categories/new.html.haml
%h2 Nueva categoria
- form_for(@category, :html => {:id => 'new_category'}) do |f|
%p
= f.label :name, 'Nombre:'
= f.text_field :name
= f.submit 'Agregar categoria'
%ul#categories
= render :partial => @categories
|
y el partial para la categoria quedaria asi:
1 2 3 4 5 |
-#app/views/categories/_category.html.haml
%li[category]
== #{category.name}
= link_to 'Editar', edit_category_path(category)
= link_to 'Eliminar', {:action => 'destroy_nojs', :id => category}, :class => 'delete_category'
|
Entonces lo que faltaria seria ajaxificar el formulario y hacer algo despues que se creo la categoria, el archivo public/javascripts/application.js ahora quedaria asi:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
$.fn.delete_link = function(msg_confirm, message){
$(this).livequery("click", function() {
if (confirm(msg_confirm)) {
var ajaxTarget = $(this).attr("href").replace(/destroy_nojs\//,"") + ".js";
$.post(ajaxTarget, { _method: "delete" },
function(data){
alert(message);
});
$(this).parent().fadeOut("slow");
}
return false;
});
};
$(document).ready(function(){
$("#new_category").submit(function() {
$.post($(this).attr('action') + '.js', $(this).serializeArray(), null, 'script');
return false;
});
$(".delete_category").delete_link("Seguro que desea eliminar esta categoria?", "Categoria eliminada con éxito");
});
|
Hasta aqui ya hemos ajaxificado el formulario, ahora falta realizar algo despues que se creo la categoria, lo que haremos es simplementemente agregar un nuevo elemento a la lista con identifciador #categories y agregarle un pequeño efecto, la vista para la acción create quedaria asi:
1 2 3 4 |
#app/views/categories/create.js.erb
$('#categories').append('<%= escape_javascript(render(:partial => @category)) %>');
$('#new_category_form :text').val("");
$('#<%= dom_id(@category) %>').effect("highlight");
|
Y bien eso ha sido todo, no es nada dificil cuando se tiene un conocimiento adecuado de Jquery, el cual he estado obteniendo en estos ultimos dias y lo mas importante es que logras poner cada cosa en su lugar, el HTML donde debe estar, el CSS tambien y el Javascript igual, esto ayuda bastante cuando se termina la aplicación y hay que hacer mejoras o agregar nuevas cosas, espero que este articulo haya sido de utilidad a alguien.
Preguntas para contratar a un desarrollador ideal
Alexey Kovyrin uno de los desarrolladores de Scribd nos muestra algunas de las preguntas que ellos hacen para contratar a un desarrollador para este tipo de aplicaciones, osea aplicaciones que manejan un alto tráfico, esto nos sirve de mucho a los desarrolladores ya que nos permite saber en que estado se encuentran nuestros conocimientos y cuanto camino falta por recorrer para algún dia tomar una de estas posiciones.
Curso de git y github en 1 semana!
Me acabo de enterar algo tarde pero de todas maneras vale, el dia de mañana empezara un curso gratuito y muy bueno donde aprenderas cosas relacionadas a git y github, no dejes pasar esta oportunidad!
Curso excelente y gratuito de Ruby, JRuby y Rails
En la lista argentina de Ruby enviaron información sobre un excelente sitio donde oferecen varios cursos gratuitos y entre ellos enseñan Ruby, JRuby y Rails creo que esto le será de mucho provecho para aquellos que quieran iniciarse en este marvilloso lenguaje.
Detectando pais del usuario por IP
Obtener el pais del usuario a traves de su IP es una tarea realmente sencilla gracias a los servicios que proveen terceros para tal fin, pero lo que nos puede tomar tiempo es encontrar un servicio que sea decentemente fiable al momento de la detección del pais.
Como trabajan estos servicios?
Cada pais tiene un pool de rangos de IPs asignados para el, este pool esta en constante actualización supongo que es debido a que los usuarios de Internet van en aumento.
El navegador al momento de hacer una petición a un sitio envia ciertas cabeceras que es como un Hash que contiene información al estilo clave-valor, entre una de esas cabeceras se encuentra la ip del usuario quien visita el sitio, entonces al obtener la IP del usuario no queda mas que consultar contra ese pool de direcciones IP que tenemos y ver a que pais pertenece esa IP
Servicios que he probado y por tanto puedo recomendar
El primer servicio que probe se llama Find IP Address lo bueno que tiene este servicio es que te otorga el rango de IPs para el pais que tu eligas, pudiendo luego exportarlo a tu base de datos y realizar el trabajo de detección de país. El problema con este servicio es que no es tan fiable como parecia inicialmente, asi es que tuve que seguir “huaqueando”.
El otro servicio que probé y finalmente me convenció fue el de la empresa Maxmind, llegue a esta empresa por medio de la página de Mysql, que en algun lugar(en este momento no recuerdo) te da informacióm referente a tu IP y usa el servicio de esta empresa.
Ahora si al grano
Maxmind tiene un servicio denominado GeoIP Country el cual es de pago, pero tiene otro similar llamado Geolite la principal diferencia radica en la precisión 99.8% del servicio de pago contra 99.5% del gratuito, la verdad no se que tanto impacto pueda tener esta diferencia, pero he venido probando el Geolite y hasta ahora todo va de perlas.
Integrando el servicio con tu aplicación Rails
El servicio tiene disponibles “APIs para varios lenguajes:http://www.maxmind.com/app/api entre el cual esta nuestro favorito, pues bien a proceder con la integración:
Paso 1: Descargar la libreria escrita en C para el Geoip.
Luego procedemos con la instalacióm, lo cual es trivial para aquellos que vienen usando linux un par de meses:
1 2 3 4 5 |
tar -xvzf GeoIP.tar.gz cd GeoIp ./configure make sudo make install |
Paso 2: Descargar la libreria para Ruby.
Igualmente a lo anterior, en la consola lo descomprimimos y pasamos a la instalación:
1 2 3 4 5 |
tar -xvzf net-geoip-0.06.tar.gz cd net-geoip-0.06 ruby extconf.rb --with-geoip-include=/usr/local/include make sudo make install |
Si al momento de la instalación te sale algún error, entonces hay que editar el archivo Makefile y reemplazar una cadena que dice “Wall-g” por “Wall -g”, el error es simplemente por la falta de la separación mediante el espacio en blanco para los parametros.
Paso 3:
Finalmente tenemos que descargarnos la base de datos con los rangos de IPs en formato binario, igualmente procedemos a descomprimirlo y a copiarlo en algun lugar de nuestra aplicación:
1 2 |
gzip -d GeoIP.dat.dz cp GeoIP.dat /ruta/a/tu/rails_app/db/ |
En este caso yo lo he copiado dentro del directorio db de mi aplicación, tu puedes copiarlo donde creas conveniente.
Paso 4:
Ahora crearemos un initializer donde declararemos una constante la cual servira de referencia para acceder a las funciones de la libreria:
1 2 |
#en config/initializers/geo_ip.rb GEOIP = Net::GeoIP.open(File.join(Rails.root,'db','GeoIP.dat'), Net::GeoIP::TYPE_RAM) |
Paso 5:
Finalmente podemos crear un filtro para tener disponible el codigo de pais del usuario en toda nuestra aplicación:
1 2 3 4 5 6 7 8 9 10 11 12 |
#en app/controllers/application.rb class ApplicationController < ActionController::Base before_filter :current_country helper_method :current_country #por si quieres usarlo en tus vistas protected def current_country GEOIP.country_code_by_addr(request.remote_ip) end end |
Y bien, eso ha sido todo, como ven no es muy complicado poder saber el pais del visitante de nuestro sitio, espero que haya sido de utilidad este artículo.
Aqui estoy porque no me he ido
Asi es amables lectores, me he ausentado demasiado tiempo del blog y motivos he tenido, el principal es el trabajo y el secundario mi vida, creo que me tengo que dar mas tiempo aun para vivir el mundo real, pero bueno eso es tema muy aparte.
Lo importante es que he decidido volver a escribir en mi blog y en esta oportunidad de manera mas activa y con un sistema de blogs muy sencillo y extensible: Enki.
Bueno lo mio no es escribir este tipo de posts, me levanto, almuerzo y duermo pensando en Ruby, Rails y demas hierbas asi es que esten atentos que ahora si blogueare mas seguido.