homeASCIIcasts

211: Las validaciones en Rails 3 

(view original Railscast)

Other translations: En It

Other formats:

Written by Juan Lupión

En este episodio veremos las mejoras que Rails 3 ha introducido en las validaciones, pero antes de eso actualizaremos Rails 3 a la nueva beta, la 3.

Actualización de Rails 3

Podemos instalar esta nueva versión igual que hemos hecho con las anteriores ejecutando:

gem install rails --pre

Es posible que tras la instalación de la nueva beta tengamos problemas al ejecutar el comando rails y veamos un mensaje de error como este:

$ rails store
/Users/eifion/.rvm/gems/ruby-head/gems/rails-3.0.0.beta3/bin/rails:1:in `require': no such file to load -- rails/cli (LoadError)
	from /Users/eifion/.rvm/gems/ruby-head/gems/rails-3.0.0.beta3/bin/rails:1:in `<top (required)>'
	from /Users/eifion/.rvm/gems/ruby-head/bin/rails:19:in `load'
	from /Users/eifion/.rvm/gems/ruby-head/bin/rails:19:in `<main>'

Se trata de un problema de compatibilidad hacia atrás con las versiones beta anteriores. Para corregirlo podemos hacer

gem cleanup

lo que eliminirá las versiones antiguas de las gemas instaladas. Ya se puede ejecutar el comando rails, con lo que podemos crear la aplicación que usaremos para este episodio.

rails store

La nueva funcionalidad

Con nuestra aplicación en marcha ya podemos empezar el repaso de las nuevas funcionalidades de validación. Comenzaremos generando un andamiaje para el modelo User que tendrá dos atributos: el nombre y la dirección de correo.

rails g scaffold user name:string email:string

Y migraremos la base de datos como es habitual.

rake db:migrate

Veamos el código que muestra la validación, que reside en el parcial del formulario que se ha generado automáticamente.

/app/views/users/_form.html.erb

<%= form_for(@user) do |f| %>
  <% if @user.errors.any? %>
  <div id="errorExplanation">
    <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2>
    <ul>
    <% @user.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
  <% end %>

  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </div>
  <div class="field">
    <%= f.label :email %><br />
    <%= f.text_field :email %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

Como se ve, el código que muestra la información de error es bastante abudante. La razón es que en la última versión ha desaparecido el método error_messages_for y tampoco podemos hacer

<%= f.error_messages %>

Estos métodos han sido sacados a su propio plugin, dynamic_form. Bastará con instalar dicho plugin para recuperar todos estos métodos. Sin embargo no se recomienda hacerlo.

La razón por la que estos métodos se han eliminado es porque con frecuencia los mensajes de error se deben personalizar y hacerlo con los métodos antiguos era un poco incómodo y no tan flexible como teniendo el marcado del mensaje de error en línea como ahora: teniendo el HTML a mano en las vistas significa que podemos hacer los cambios en la visualización de mensajes de error como queramos.

Claro está que el tener el código de mensajes de error en línea en el formulario para cada modelo puede conducirnos a duplicar mucho código. Para reducir esto podemos extraer el código de error a su propio parcial de forma que podamos usar dicho parcial en todos los modelos. Lo pondremos en una carpeta shared y lo llamaremos _error_messages.html.erb pasándole el objeto destino (que en nuestro caso será nuestro usuario).

/app/views/users/_form.html.erb

<%= form_for(@user) do |f| %>
  <%= render "shared/error_messages", :target => @user %>
  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </div>
  <div class="field">
    <%= f.label :email %><br />
    <%= f.text_field :email %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

En el nuevo parcial pegaremos el código que hemos quitado del formulario, renombrando la variable @user por target para que haga referencia al objeto que se le esté pasando. También cambiaremos el mensaje de error para que sea más genérico.

/app/views/shared/_error_messages.html.erb

<% if target.errors.any? %>
<div id="errorExplanation">
  <h2><%= pluralize(target.errors.count, "error") %> prohibited this record from being saved:</h2>
  <ul>
  <% target.errors.full_messages.each do |msg| %>
    <li><%= msg %></li>
  <% end %>
  </ul>
</div>
<% end %>

Podemos probar el nuevo parcial añadiendo una validación al atributo email de nuestro modelo User.

/app/models/user.rb

class User < ActiveRecord::Base
  validates_presence_of :email
end

Si vamos al formulario de nuevo usuario y lo enviamos sin haber rellenado el campo para el correo veremos los errores de validación:

Mensajes de error de nuestro parcial.

Así pues vemos que es muy fácil extraer el código para mostrar mensajes de error a un parcial para que todo siga siendo flexible. Si queremos continuar con los viejos métodos helper, podemos emplear una técnica parecida a la usada por Ryan Bates en sus Nifty Generators, que tienen un módulo llamado ErrorMessagesHelper module que incluye los métodos error_messages_for y error_messages en los constructores de formulario. Son versiones simplificadas de los métodos que proporcionaba Rails y son fáciles de ajustar a nuestras necesidades, por lo que tal vez nos podría interesar el incluir dichos métodos a nuestra aplicación.

Reflexión de la validación

Una nueva funcionalidad de Rails 3 es la capacidad de reflexión sobre la validación. Podemos utilizarla si, por ejemplo, queremos poner un asterisco junto a cada campo requerido del formulario. Anteriormente teníamos que instalar un plugin externo para poder determinar qué validaciones aplicaban sobre el modelo para el cual se mostraba un formulario. Ahora esto viene incluido en Rails por lo que es mucho más fácil incluir un indicador junto a cada campo.

Esto es más fácil de mostrar con la consola. Podemos invocar a validators contra un modelo para recuperar un array de las validaciones aplicadas sobre dicho modelo.

ruby-head > User.validators
 => [#<ActiveModel::Validations::PresenceValidator:0x00000100badfc8 @attributes=[:email], @options={}>]

Como sólo hemos establecido una validación en nuestro modelo User veremos que tan sólo se devuelve un validador que será de tipo PresenceValidator porque la validación es del tipo validates_presence_of.

Podemos recuperar los validadores para un atributo utilizado el método validators_on:

ruby-head > User.validators_on(:email)
 => [#<ActiveModel::Validations::PresenceValidator:0x00000100badfc8 @attributes=[:email], @options={}>]

Esto devolverá el mismo validador que antes porque es el único que tenemos, actuando sobre el atributo email. Si invocamos este método contra el atributo name veremos que se nos devuelve un array vacío.

ruby-head > User.validators_on(:name)
 => []

En teoría nos interesa ejecutar estas comprobaciones en el constructor del formulario para que se añada automáticamente el asterisco de la manera más eficiente, pero hacer esto está fuera del alcance de este episodio así que simplemente crearemos un helper propio llamado mark_required. Este método recibirá dos parámetros: un modelo y un atributo para dicho modelo. Por ejemplo, para el atributo email invocaríamos así el método:

<%= mark_required(@user, :email) %>

Podemos crear el método mark_required en el fichero ApplicationHelper.

/app/helpers/application_helper.rb

module ApplicationHelper
  def mark_required(object, attribute)
    "*" if object.class.validators_on(attribute).map(&:class).include? ActiveModel::Validations::PresenceValidator
  end
end

El método devolverá una cadena con un asterisco si PresenceValidator está entre los validadores del modelo. Una vez que tenemos este método podemos aplicarlo a cada campo en el formulario:

/app/views/users/_form.html.erb

<%= form_for(@user) do |f| %>
  <%= render "shared/error_messages", :target => @user %>
  <div class="field">
    <%= f.label :name %><%= mark_required(@user, :name) %><br />
    <%= f.text_field :name %>
  </div>
  <div class="field">
    <%= f.label :email %><%= mark_required(@user, :email) %><br /> 
    <%= f.text_field :email %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

Como mencionábamos antes, esta no es la mejor manera de hacerlo porque tenemos que añadir manualmente una llamada al método contra cada campo (extender FormBuilder para hacer esto no debería ser demasiado difícil).

Si recargamos el formulario veremos aparecer un asterisco junto al campo email pero no junto al campo name.

Ahora aparece un asterisco junto a los campos requeridos.

Validaciones en la capa del modelo

Hasta ahora tan sólo hemos cubierto validaciones en la vista, pero las mejoras en las validaciones a nivel de modelo también son excelentes. Merece la pena, para entrar en detalle, leer este artículo en el blog de Mike Lindsaar. Una diferencia importante es que se han sacado las validaciones de ActiveRecord y se han puesto en ActiveModel. Con esto es más fácil añadir validaciones a objetos no ActiveRecord, y profundizaremos en esto en un próximo episodio.

Donde nos vamos a centrar es en que las validaciones de nuestro modelo User sean más eficientes. Hemos añadidos un par de validaciones al modelo de forma que ahora tiene el siguiente aspecto:

/app/models/user.rb

class User < ActiveRecord::Base
  validates_presence_of :email
  validates_uniqueness_of :email
  validates_format_of :email, :with => /^[\w\d]+$/ :on => :create, :message => "is invalid"
end

En Rails 3 es posible invocar el método validates y pasarle un hash de atributos para definir las validaciones, en lugar de definir cada validación por separado.

/app/models/user.rb

class User < ActiveRecord::Base
  validates :email, 
            :presence => true, 
            :uniqueness => true, 
            :format => { :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i }
end

En el modelo User seguimos validando que el campo tenga un valor y que dicho valor sea único. Para validar el formato hay varias opciones que podemos pasar, así que usamos un hash secundario para definirlas.

Podemos pasar cualquier número de validaciones para un atributo con un único comando. Si bien esto es bastante útil en la mayoría de situaciones, puede llegar a ser poco manejable si tenemos muchas validaciones.

Cómo escribir nuestros propios validadores

Veamos ahora cómo podemos hacer que la opción :format sea más concisa. Nos interesará frecuentemente validar direcciones de correo y tener la misma kilométrica expresión regular en cada validador es poco elegante e introduce una repetición innecesaria en el código. Podemos extraer esto a su propia validación creando una clase en el directorio /lib de nuestra aplicación. Lo llamaremos email_format_validator.rb.

/lib/email_format_validator.rb

class EmailFormatValidator < ActiveModel::EachValidator
  def validate_each(object, attribute, value)
    unless value =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
      object.errors[attribute] << (options[:message] || "is not formatted properly")
    end
  end
end

La clase EmailFormatValidator hereda de ActiveModel::EachValidator. Tenemos que definir un método llamado validate_each, que recibirá tres parámetros llamadosobject, attribute y value y comprobará que el valor casa con la expresión regular que usamos para validar direcciones de correo. Si esto no es así, añadirá el atributo a errors.

Con esta técnica podemos definir cualquir tipo de validación que deseemos (por ejemplo, en la anotación del blog mencionado anteriormente se utiliza un conjunto más completo de validaciones sobre la dirección de correo). Ahora que ya tenemos nuestro propio validador podemos utilizarlo en el modelo User.

/app/models/user.rb

class User < ActiveRecord::Base
  validates :email, :presence => true, :uniqueness => true, :email_format => true
end

La presencia de la clave email_format en el hash validates singifica que el validador buscará una clase llamada email_format_validator y pasará el comportamiento de validación a la clase que acabamos de escribir.

Si creamos un usuario e introducimos una dirección de correo incorrecta ahora vereoms el siguiente mensaje de error.

Clase de error que muestra un mensaje de error.

Con la posibilidad de crear nuestros propios validadores podemos reorganizar las validaciones en nuestros modelos y hace que sea fácil eliminar cualquier duplicación en las validaciones.