Mejorando el $log de AngularJS con decorators

La idea de este artículo es contar con un servicio de logging que nos permita contextualizar fácilmente la información que aparece en la consola del navegador.

A continuación pongo unos ejemplos para ilustrar su uso:

Uso básico:

$log.debug( 'línea debug' );
$log.info( 'línea info' );

Salida:

[0] -- 12/3/2016 15:43:38 línea debug
[1] -- 12/3/2016 15:43:38 línea info

Uso con contextos:

var logger = $log.getLogger( 'MainController' );
logger.debug( 'línea 1' );
logger.debug( 'línea 2' );
logger.debug( 'línea 3' );
 
var logger2 = $log.getLogger( 'App' );
logger2.debug( 'otra línea' );
logger2.debug( 'una línea más' );

Salida:

[0] -- 12/3/2016 15:58:05 -- [MainController]:0>  línea 1
[1] -- 12/3/2016 15:58:05 -- [MainController]:1>  línea 2
[2] -- 12/3/2016 15:58:05 -- [MainController]:2>  línea 3
[3] -- 12/3/2016 15:58:05 -- [App]:0>  otra línea
[4] -- 12/3/2016 15:58:05 -- [App]:1>  una línea más

Con desactivación de contextos:

var logger = $log.getLogger( 'MainController' );
logger.enable( false );
logger.debug( 'línea 1' );
logger.debug( 'línea 2' );
logger.enable( true );
logger.debug( 'línea 3' );
 
var logger2 = $log.getLogger( 'App' );
logger2.debug( 'otra línea' );
logger2.debug( 'una línea más' );

Salida:

[0] -- 12/3/2016 16:02:00 -- [MainController]:0>  línea 3
[1] -- 12/3/2016 16:02:00 -- [App]:0>  otra línea
[2] -- 12/3/2016 16:02:00 -- [App]:1>  una línea más

Los servicios en AngularJS se pueden mejorar de dos maneras: Usando decorators o providers.

La ventaja de usar un decorator es que sólo necesitas declararlo dentro del módulo de tu aplicación o incluir el módulo dónde está declarado como dependencia, para que entonces se decore el servicio deseado.

El código presentado a continuación utiliza un decorator:

( function() {
  'use strict';
 
  // Asume que el módulo de tu aplicación es llamado "myApp"
  // En este módulo se define el decorador
  angular.module( 'myApp' ).decorator( '$log', logDecorator );
 
  // La función "logDecorator" recibirá el servicio a decorar como
  // el parámetro $delegate, en este caso $delegate == $log
  function logDecorator( $delegate ) {
 
    // Contador general
    var n = 0;
 
    // El nuevo logger que imprime información básica
    var basicLogger = {
      log: basicDecoration( $delegate.log ),
      debug: basicDecoration( $delegate.debug ),
      info: basicDecoration( $delegate.info ),
      warn:basicDecoration( $delegate.warn ),
      error: basicDecoration( $delegate.error ),
      getLogger: getLogger,
      enabledContexts: {},
      counter: {}
    };
 
    // Esta función regresa el servicio de log básico mejorado
    return basicLogger;
 
 
    // La decoración básica incluye un contador, fecha y hora al inicio
    function basicDecoration( loggingFunction ) {
      return function() {
        var date = new Date();
        var args = Array.prototype.slice.call( arguments );
        args.unshift( '[' + n++ + '] -- ' + date.toLocaleString() );
        loggingFunction.apply( null, args );
      }
    }
 
    // La decoración contextual incluye info del contexto y un contador
    function contextDecoration( loggingFunction, context ) {
      return function() {
        var enabled = basicLogger.enabledContexts[context];
        if( enabled || enabled === undefined ) {
          var args = Array.prototype.slice.call( arguments );
          var contextInfo = '-- [' + context + ':';
          var counterInfo = basicLogger.counter[context]++ + '] >> ';
          args.unshift( contextInfo + counterInfo );
          loggingFunction.apply( null, args );
        }
      }
    }
 
    // Activa o desactiva el logging en un contexto determinado
    function enable( context ) {
      return function( enable ) {
        basicLogger.enabledContexts[context] = enable;
      }
    }
 
    // Regresa un logger para un contexto determinado
    function getLogger( context ) {
 
      // Contador contextual
      basicLogger.counter[context] = 0;
 
      // La nueva instancia de logger con información del contexto
      var contextLogger = {
        log: contextDecoration( basicLogger.log, context ),
        debug: contextDecoration( basicLogger.debug, context ),
        info: contextDecoration( basicLogger.info, context ),
        warn: contextDecoration( basicLogger.warn, context ),
        error: contextDecoration( basicLogger.error, context ),
        enable: enable( context )
      };
 
      // Esta función regresa el servicio de log contextualizado
      return contextLogger;
    }
  }
} )();

Una recomendación es que siempre utilices el servicio de AngularJS $log en vez del console.log, de esta manera siempre podrás desactivar todo el logging desde el bloque de configuración de tu aplicación con algo como esto:

angular.module( 'myApp', [] ).config( config );
 
function config( $logProvider ) {
  $logProvider.debugEnabled( false );
}

Con este nuevo logger también puedes mandar a consola toda actividad en tus controladores, directivas, servicios, etc., y si la información que aparece es demasiada, puedes desactivar los contextos que gustes desde el bloque de ejecución de tu aplicación.

Digamos que tienes un controlador llamado MainController, y en él inicializas un logger con contexto “MainController”. Para desactivar toda la salida a consola generada por ese controlador, puedes hacer algo así:

angular.module( 'myApp', [] ).config( config ).run( run );
 
function config( $logProvider ) {
  $logProvider.debugEnabled( true );
}
 
function run( $log ) {
  var logger = $log.getLogger( 'MainController' );
  logger.enable( false );
}

La idea es que en cada componente de tu aplicación tengas un logger y el contexto sea el nombre del componente.

Referencias

Enhancing AngularJS Logging using Decorators
Enhancing $log in AngularJs the simple way

Configuración de una aplicación Rails 4.2 con MongoDB para una JSON API

En esta guía se explica cómo crear y configurar una aplicación Rails con MongoDB, de tal manera que funcione como una JSON API, es decir, puro back end.

Una aplicación de este tipo se da muy bien para después conectarla con un cliente SPA (Single Page Application), digamos en AngularJS o para emplearla con una aplicación móvil hecha con Ionic, por ejemplo.

Se asume familiaridad con Rails, git y los códigos de estado HTTP.

Preliminares

Commit inicial

Para generar el proyecto y hacer el primer commit:

rails new json_api --skip-active-record
cd json_api
git init
git add .
git ci -m "Commit inicial."

La opción de –skip-active-record desactiva ActiveRecord porque usaremos MongoDB con Mongoid en lugar de una base de datos relacional como PostgreSQL o MySQL.

Configuración inicial

Agrega estas gemas a tu Gemfile:

group :development, :test do
  ...
  # Framework para pruebas
  gem 'minitest-rails'
  # Para generar objetos de prueba
  gem 'fabrication'
  # Para generar datos de prueba
  gem 'faker'
end
 
# Para usar MongoDB a través de Mongoid
gem 'mongoid'
# Para usar respond_with en los controladores
gem 'responders'
# Para usar ActiveModel#has_secure_password
gem 'bcrypt'

Después en la terminal:

bundle
rails g mongoid:config
rails g minitest:install

Con esto se instalan las gemas que hagan falta, después se generan los archivos de configuración para Mongoid y Minitest. No es necesario correr el generador para los responders.

Al ejecutar el generador de Minitest, te va a preguntar si debe sobreescribir el archivo test_helper.rb, contesta sí.

En el archivo config/mongoid.yml, quita el comentario de la línea que aparece a continuación y cambia el valor a false:

# raise_not_found_error: true
raise_not_found_error: false

No queremos que Mongoid levante una excepción toda vez que no encuentre un documento.

Es deseable tener más control sobre cuándo se lanza una excepción, de esta forma, cuando se busca un usuario para autenticación y no existe, en vez de un código de error 404 (Not Found) podemos mandar un 401 (Unauthorized).

Agrega este contenido al archivo config/application.rb:

config.generators do |g|
  # Framework de pruebas estilo BDD
  g.test_framework :minitest, spec: true, fixture: true, fixture_replacement: :fabrication
 
  # Estamos haciendo una API, no queremos assets, helpers, tampoco views
  g.assets false
  g.helper false
  g.template_engine nil
end
 
# Para manejar la presentación de errores 404 y 500 como JSON
config.exceptions_app = self.routes

En el archivo test/test_helper.rb, comenta las siguientes líneas:

# ActiveRecord::Migration.check_pending!
# fixtures :all

No tenemos disponible ActiveRecord, si no se comentan esas líneas al tratar de ejecutar las pruebas se lanzará una excepción.

En el archivo config/initializers/inflections.rb agrega:

ActiveSupport::Inflector.inflections( :en ) do |inflect|
  inflect.acronym 'API'
end

Esto permite que al generar los controladores se utilice API como nombre del módulo contenedor en lugar de Api.

Configuración de rutas y del controlador principal de la aplicación

En el archivo config/routes.rb agrega:

match '/404' => 'application#not_found', via: [:get, :post, :put, :patch, :delete]
match '/500' => 'application#exception', via: [:get, :post, :put, :patch, :delete]

Esto permite personalizar las respuestas de error cuando no se encuentra alguna ruta o cuando se tiene algún error en la aplicación.

Por defecto, en estos casos lo que se devuelve es HTML (y el código de estado HTTP adecuado), pero estamos interesados en siempre entregar JSON al cliente.

En el archivo app/controllers/application_controller.rb cambia :exception por :null_session y agrega:

protect_from_forgery with: :null_session
 
# Estos métodos se llaman desde las rutas
def not_found
  response = http_error( { status: '404', message: 'Not Found' } )
  render json: response, status: :not_found
end
 
def exception
  response = http_error( { status: '500', message: 'Internal Server Error' } )
  render json: response, status: :internal_server_error
end
 
 
private
# Método de utilería
def http_error( error )
  { error: { http: error } }
end

La línea de protect_from_forgery_with indica el tipo de protección que se utilizará como medida de prevención contra un ataque de tipo CSRF.

Cuando es :exception se lanza una excepción ante cualquier petición POST, PUT, PATCH o DELETE si no viene acompañada de un token especial que Rails normalmente inserta en las vistas. Siendo esta una aplicación que va a ser utilizada como API no cuenta con ellas.

De la documentación: ActionController::RequestForgeryProtection::ClassMethods

Turn on request forgery protection. Bear in mind that GET and HEAD requests are not checked.

Con :null_session se indica que no habrá estado del lado del servidor, por tanto, no existe una sesión que mantega el estado de conectado dentro del sistema. La autenticación se logra por medio de un token especial que se enviará desde el cliente en la cabecera de la petición. Más adelante veremos cómo.

Otros errores que sería bueno atrapar

Igual, en el archivo app/controllers/application_controller.rb agrega:

# Para cuando lancemos la excepción de documento no encontrado en MongoDB
rescue_from Mongoid::Errors::DocumentNotFound, with: :document_not_found
# Para cuando el tipo de contenido solicitado no sea application/json
rescue_from ActionController::UnknownFormat, with: :not_acceptable
# Para cuando hagan falta parámetros requeridos en algún modelo
rescue_from ActionController::ParameterMissing, with: :unprocessable_entity
 
def document_not_found
  response = http_error( { status: '404', message: 'Document Not Found' } )
  render json: response, status: :not_found
end
 
def not_acceptable
  response = http_error( { status: '406', message: 'Not Acceptable' } )
  render json: response, status: :not_acceptable
end
 
def unprocessable_entity
  response = http_error( { status: '422', message: 'Unprocessable Entity' } )
  render json: response, status: :unprocessable_entity
end
 
def unauthorized
  response = http_error( { status: '401', message: 'Unauthorized' } )
  render json: response, status: :unauthorized
end

Con la configuración inicial terminada, agrega los cambios al repo:

git add .
git ci -m "Configuración inicial."

Testing

TDD vs BDD

La diferencia entre los dos es el cómo te gusta pensar acerca del modelado de la aplicación.

El TDD utiliza el es (assert, afirma), el BDD usa el debe (must, debe). Básicamente es una cuestión de es vs debe ser.

Probemos primero con un poco de BDD

Genera tu primer controlador

rails g controller API::Users

Ejecuta y observa:

rake test

Edita test/controllers/api/users_controller_test.rb y cambia el test de ejemplo por:

it "#index debe ser exitoso" do
  get :index
  response.status.must_equal 200
end

Ejecuta y observa:

rake test

Edita config/routes.rb para agregar el recurso:

namespace :api do
  resources :users
end

Ejecuta y observa:

rake test

Edita app/controllers/api/users_controller.rb y agrega:

def index
  render json: {}
end

Ejecuta y observa:

rake test

😀

Esta fue una muestra de un proceso muy básico de BDD para un controlador.

Genera tu primer modelo

rails g model User name:string email:string password_digest:string

Ejecuta y observa:

rake test

Abre el archivo test/models/user_test.rb y verás:

require 'test_helper'
 
describe User do
  let( :user ) { User.new }
 
  it "must be valid" do
    user.must_be :valid?
  end
end

Si analizamos un poco la situación y contemplamos nuestro modelo User, veremos que el usuario forzosamente necesita al menos un correo. Así, un usuario recién creado debería ser inválido.

Cambia el único test que existe en ese archivo por este otro:

it 'no debe ser válido' do
  user.wont_be :valid?
end

Ejecuta y observa:

rake test

Abre el archivo app/models/user.rb y agrega:

validates :email, presence: true

Ejecuta y observa:

rake test

😀

Y esta fue una muestra de un proceso muy básico de BDD para un modelo.

A continuación veremos cómo se haría con TDD.

Probemos ahora con un poco de TDD

Para que las pruebas se generen al estilo TDD cambia spec a false:

g.test_framework :minitest, spec: false, fixture: false

Genera tu primer controlador

rails g controller API::Users

Ejecuta y observa:

rake test

Edita test/controllers/api/users_controller_test.rb y cambia el test de ejemplo por:

def test_index_action
  get :index
  assert_response :success
end

Nota: El nombre de los métodos debe comenzar necesariamente con test_ para que sean ejecutados durante el proceso de pruebas.

El método anterior es equivalente a este:

test 'index action' do
  get :index
  assert_response :success
end

Ejecuta y observa:

rake test

Edita config/routes.rb para agregar el recurso:

namespace :api do
  resources :users
end

Ejecuta y observa:

rake test

Edita app/controllers/api/users_controller.rb y agrega:

def index
  render json: {}
end

Ejecuta y observa:

rake test

😀

Esta fue una muestra de un proceso muy básico de TDD para un controlador.

Genera tu primer modelo

rails g model User name:string email:string password_digest:string

Ejecuta y observa:

rake test

Abre el archivo test/models/user_test.rb y verás:

require 'test_helper'
 
class UserTest < ActiveSupport::TestCase
 
  def user
    @user ||= User.new
  end
 
  def test_valid
    assert user.valid?
  end
 
end

Si analizamos un poco la situación y contemplamos nuestro modelo User, veremos que el usuario forzosamente necesita al menos un correo. Así, un usuario recién creado debería ser inválido.

Cambia el único test que existe en ese archivo por este otro:

def test_invalid
  assert_not user.valid?
end

Ejecuta y observa:

rake test

Abre el archivo app/models/user.rb y agrega:

validates :email, presence: true

Ejecuta y observa:

rake test

😀

Y esta fue una muestra de un proceso muy básico de TDD para un modelo.

En lo personal prefiero el BDD, siento que en ese estilo se expresa mejor la intención de lo que quieres programar.

Finalmente

Más adelante publicaré una guía acerca de cómo hacer pruebas de integración y cómo utilizar fabricators de manera efectiva en las pruebas de modelos y controladores.

Espero esta guía te sea útil.

Repositorio en github

lobo-tuerto/rails_json_api

Configuración de una aplicación Rails 4.2 tradicional

Actualmente existen dos enfoques para desarrollar una aplicación web, el tradicional, que es impulsado por frameworks como Rails y el de las JSON API + SPA.

En el enfoque tradicional tenemos todo nuestro código en una aplicación Rails. En ella toda la lógica de negocios está en los modelos, el cómo la operamos en los controladores y la interfaz de usuario en las vistas. Programamos las vistas con Slim, Ruby y Sass, usamos gemas como simple_form para crear fácilmente formularios en Ruby.

El enfoque JSON API + SPA, por otra parte, se cuenta con dos componentes: La aplicación servidor (API) y un cliente Javascript (o móvil) para la vista.

En principio, puede parecer atractiva la idea de hacer las vistas con Ruby y sus amigotes, pero la verdad es que no hay como trabajar en el ambiente natural del lenguaje donde se despliegan nuestras aplicaciones: usualmente Javascript.

En principio, también, puede parecer meh trabajar con HTML y amigos, sin embargo cuando descubres cosas como Grunt, Gulp, Yeoman, Bower, AngularJS y demás, —Y— lo que te permiten hacer en cuestión de organización de tu proceso de desarrollo, simplemente no puedes dar marcha atrás.

El desarrollo del cliente (UI) es en sí, un universo propio, y requiere de herramientas adecuadas para poderlo desarrollar de adecuadamente.

Ambos enfoques me agradan y he usado ambos en varios proyectos. Sin embargo, me siento más atraído hacia las JSON API + SPA debido a la clara separación entre front end y back end.

Preliminares

Commit inicial

Para generar el proyecto y hacer el primer commit:

rails new rails_app --database=postgresql
cd rails_app
git init
git add .
git commit -m "Commit inicial."

Configuración inicial

Agrega estas gemas a tu Gemfile:

group :development, :test do
  ...
  # Framework para pruebas
  gem 'minitest-rails'
  # Para generar objetos de prueba
  gem 'fabrication'
  # Para generar datos de prueba
  gem 'faker'
end
 
# Escribe las vistas en Slim en vez de HTML
gem 'slim-rails'
# Crea formularios de manera concisa
gem 'simple_form'

Después en la terminal:

bundle
rails g minitest:install
rails g simple_form:install

Modifica config/application.rb:

config.time_zone = 'Mexico City'
config.i18n.default_locale = 'es-MX'
 
config.generators do |g|
  g.test_framework :minitest, :spec => true, :fixture => false
end

Crea un usuario y una base de datos para esta aplicación, revisa ls guía: Cómo instalar PostgreSQL en Ubuntu

Configura el host, username y password en config/database.yml:

default: &default
  adapter: postgresql
  encoding: unicode
  host: localhost
  user: usuario_ejemplo
  password: xxx

Ejecuta en la terminal:

rake db:create

Descarga el archivo es-MX.yml y ponlo en: config/locales/es-MX.yml
Lista de localizaciones para Rails

Modifica config/initializers/inflections.rb:

ActiveSupport::Inflector.inflections(:en) do |inflect|
  inflect.clear
 
  inflect.plural(/$/, 's')
  inflect.plural(/([^aeéiou])$/i, '\1es')
  inflect.plural(/([aeiou]s)$/i, '\1')
  inflect.plural(/z$/i, 'ces')
  inflect.plural(/á([sn])$/i, 'a\1es')
  inflect.plural(/é([sn])$/i, 'e\1es')
  inflect.plural(/í([sn])$/i, 'i\1es')
  inflect.plural(/ó([sn])$/i, 'o\1es')
  inflect.plural(/ú([sn])$/i, 'u\1es')
 
  inflect.singular(/s$/, '')
  inflect.singular(/es$/, '')
 
  inflect.irregular('el', 'los')
end

Realiza el commit con los cambios en la configuración.

git add .
git ci -m "Configuración inicial."

¡Listo, a desarrollar! 😀

Recursos

Inflexiones en español para Rails
Configurar usuario y base de datos en PostgreSQL

Un par de problemas con Passenger fáciles de resolver

Si al instalar Passenger con:

  1. gem install passenger

Te sale el siguiente mensaje:

  1. ...
  2. ** Execute (dry run) nginx
  3. /home/tlacaelel/.rvm/gems/ruby-1.9.2-p180/gems/passenger-3.0.7/lib/phusion_passenger/standalone/runtime_installer.rb:224:in `*': negative argument (ArgumentError)
  4. ...

Se resuelve al reintentar la instalación, teclea de nuevo:

  1. gem install passenger

Si después al ejecutarlo con:

  1. passenger start -p3001

Te aparece:

  1. You can stop Phusion Passenger Standalone by pressing Ctrl-C.
  2. ===============================================================================
  3. Cannot execute '/var/lib/passenger-standalone/3.0.7-x86-ruby1.9.2-linux-gcc4.5.2-1002/support/helper-scripts/prespawn http://0.0.0.0:3001': Permission denied (13)

Lo resuelves con:

  1. sudo chmod +x /var/lib/passenger-standalone/3.0.7-x86-ruby1.9.2-linux-gcc4.5.2-1002/support/helper-scripts/prespawn

Katamari Hack

[singlepic=717,160,,,left]¿Jugaron alguna vez Katamari Damacy? ¿no? yo tampoco… 🙁
Pero ahora tenemos la oportunidad de saborear una probadita de su mecánica de juego sobre cualquier página web.

¡A rodar se ha dicho! 😀

Arrastra el siguiente enlace a tu barra de marcadores para poder activarlo sobre cualquier sitio fácilmente, o simplemente da clic aquí: ¡Katamari!

O copia y pega este código en la barra del URL de tu navegador cuando visites otra página:

  1. javascript:var i,s,ss=['http://kathack.com/js/kh.js','http://ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js'];for(i=0;i!=ss.length;i++){s=document.createElement('script');s.src=ss[i];document.body.appendChild(s);}void(0);

Te aparecerá un diálogo con opciones de configuración, presiona Start! para comenzar.
Para controlar la katamari (pelota) sólo deja presionado el botón derecho de tu ratón.

(funciona mejor en chrome)

Depredado en

Katamari Hack

undefined method `version’ for nil:NilClass — Cómo actualizar rubygems 1.5.0 a 1.5.2

Si tienes instalado rubygems 1.5.0 o 1.5.1 y tratas de actualizarlo con el comando de siempre, te encontrarás con esto:

  1. $ gem update --system
  2. Updating RubyGems
  3. ERROR:  While executing gem ... (NoMethodError)
  4.     undefined method `version' for nil:NilClass

Para arreglarlo es necesario instalar la gema rubygems-update.
Teclea lo siguiente en tu terminal:

  1. gem install rubygems-update
  2. update_rubygems

Y verás algo como esto:

  1. $ update_rubygems
  2. RubyGems 1.5.2 installed
  3.  
  4. === 1.5.2 / 2011-02-10
  5.  
  6. NOTE:  RubyGems 1.5.0 and 1.5.1 have a broken <tt>gem update --system</tt>.
  7.  
  8. To upgrade you'll need to use the manual upgrade recipe.  Using sudo/su as
  9. appropriate:
  10.  
  11.   $ gem install rubygems-update
  12.   $ update_rubygems
  13.  
  14. Bug Fixes:
  15.  
  16. * Fixed <tt>gem update --system</tt>.
  17.   RubyGems can now update itself again.

Ahora ya puedes actualizar de nuevo tu rubygems con:

  1. gem update --system

ERROR: While executing gem … (ArgumentError) undefined class/module YAML::PrivateType

Si al tratar de instalar una gema te sale el error mostrado a continuación, seguramente es porque el autor hizo un lanzamiento con rubygems 1.5.0.

  1. $ rake install
  2. (in /home/oewolf/development/gamedev/github/missile-command-ruby)
  3.   Successfully built RubyGem
  4.   Name: missile-command-ruby
  5.   Version: 0.0.6
  6.   File: missile-command-ruby-0.0.6.gem
  7. Executing "ruby -S gem install ./pkg/missile-command-ruby-0.0.6.gem":
  8. ruby -S gem install ./pkg/missile-command-ruby-0.0.6.gem
  9. ERROR:  While executing gem ... (ArgumentError)
  10.     undefined class/module YAML::PrivateType
  11. rake aborted!
  12. Command failed with status (1): [ruby -S gem install ./pkg/missile-command-...]

Entonces, lo indicado aquí es hacer un relanzamiento de la gema, una vez que hayamos actualizado a rubygems 1.5.2 o superior.

Depredado en

undefined method ‘version’ for nil:nilclass ??? hmmm…

Missing host to link to! Please provide :host parameter or set default_url_options[:host]

Al estar desarrollando una aplicación web con Rails 3 y Devise, me apareció el siguiente error al estar jugando con la característica lockable:

ActionView::Template::Error (Missing host to link to! Please provide :host parameter or set default_url_options[:host])

Investigando un poco me topé con que la solución consistía en hacer caso a la indicación que aparece en el README de Devise. Je.

Necesitamos configurar en cada ambiente (development, production, etc.) las URLs que se emplearán en el envío de correos electrónicos:

Aquí está la configuración para config/environments/development.rb:

config.action_mailer.default_url_options = { :host => 'localhost:3000' }

Y aquí una de ejemplo para config/environments/production.rb:

config.action_mailer.default_url_options = { :host => 'lobotuerto.com' }

Problemas con rake con Ruby 1.8 y Ruby 1.9 instalados

Hace un par de meses que no tengo salida de video en mi laptop, pero he podido laborar con ella por medio de un ssh -X.

Sin embargo, pienso mandarla a reparar pronto, así que decidí mudar mis proyectos de trabajo a una máquina de escritorio que tengo por ahí.

En la de escritorio usaba primariamente Ruby 1.9. y no había tocado la instalación de Ruby 1.8 desde hacía ya un buen rato.

Debo agregar que todos los proyectos del trabajo que usan Rails, lo hacen sobre Ruby 1.8. Así que me puse a instalarle todas las gemas que hacían falta, entre ellas: Rails.

Cuando terminé, accedí a uno de mis proyectos y quise ver el listado de las tareas con un simple:

rake -T

Sólo para ser presentado con un críptico:

rake aborted!
no such file to load -- cucumber/rake/task

WTF? pensé, ¿qué tiene que ver el cucumber con un rake -T?
Así que instalé el cucumber en el ambiente de Ruby 1.8 para ver si eso solucionaba el problema, y no, seguía igual.

Entonces escribí:

rake -T --trace

Y cuál sería mi sorpresa al ver:

rake aborted!
no such file to load -- cucumber/rake/task
/usr/local/lib/ruby19/gems/1.9.1/gems/activesupport-2.3.5/lib/active_support/dependencies.rb:156:in `require'

!!!
¿Por qué esta utilizando las gemas del 1.9 si tengo activado el 1.8?

Después de darme de topes un rato ubiqué el problema …

El archivo rake que se encuentra alojado en /usr/local/bin/rake indica de manera explícita que debe ejecutarse con Ruby 1.9:

#!/usr/local/bin/ruby19

Lo que hay que hacer es editarlo con:

sudo gedit /usr/local/bin/rake

Y cambiar esa línea por esta otra:

#!/usr/bin/ruby

Listo, ahora rake empleará la versión de Ruby que tengamos activada en nuestro sistema. 🙂

Una mejor manera de instalar Ruby en Ubuntu es con rbenv: Cómo instalar Ruby con rbenv en Ubuntu.