08: ACTIVE RECORD: Validaciones - LPC-Ltda/Ruby-on-Rails GitHub Wiki

He comprado esta máquina maravillosa - un ordenador. Ahora soy algo así como una autoridad en dioses, así que identifiqué la máquina - me parece ser un dios del Antiguo Testamento con un montón de reglas y sin piedad. -Joseph Campbell

El API de validaciones de Active Model, junto con su funcionalidad suplementaria en Active Record, le permite definir declarativamente estados válidos para sus objetos de modelos. Los métodos de validación enganchan dentro del ciclo de vida de un objeto de modelo Active Record y está habilitado para inspeccionar el objeto para determinar si ciertos atributos están seteados, tienen valores en un determinado rango, o pasan cualquier otro obstáculo que usted especifique.

En este capítulo, describiremos los métodos de validación disponibles y cómo usarlos efectivamente. También exploraremos cómo estos métodos de validación interactúan con sus atributos del modelo y cómo el sistema de mensajes de error empaquetados pueden ser usados efectivamente en la interface de usuario de la aplicación para proveer feedback descriptivo.

Finalmente, cubriremos cómo usar la funcionalidad de validación de Active Model por usted mismo, - clases No-Active Record.

8.1 Encontrando Errores

Los problemas de validación son también conocidos como (redoble de tambores, por favor..) errores! Cada objeto de modelo de Active Record contiene una colección de errores, accesibles (sorprendentemente) como el atributo errors.

Cuando un objeto modelo es válido, la colección errors está vacía. De hecho, cuando usted llama a valid? sobre un objeto modelo, realiza los siguientes tres pasos para buscar errores (levemente simplificado):

  1. Limpia la colección errors
  2. Corre las validaciones
  3. Retorna si la colección errors del modelo esta ahora vacía o no

Si la colección errors final está vacía, el objeto es válido. En casos donde usted tiene que escribir la lógica de validación actual por usted mismo, usted marca un objeto inválido al agregar items a la colección de errores usando su método add. Tan simple como eso.

Cubriremos los métodos de la clase Errors en más detalle después. Hace más sentido ver los métodos de validación primero.

8.2 Las validaciones declarativas simples

Cuando sea posible, usted debe setear validaciones para sus modelos declarativamente usando una o más de los siguientes métodos de clase disponibles para todas las clases Active Record. A menos que se indique lo contrario, todas los métodos validates aceptan un número variable de atributos más opciones. Hay algunas opciones para estos métodos de validaciones que son comunes para todos ellos, y serán cubiertas al final de esta sección.

8.2.1 validates_absence_of

El método validate_absence_of se asegura que atributos específicos estén en blanco. Usa el método blank?, definido sobre Object, el cual retorna true para valores que son nil o el string en blanco "". Este es el opuesto de la validación comunmente usada validates_presence_of cubierta luego en esta sección.

class Account < ActiveRecord::Base
  validates_absence_of :something_unwanted
end

Cuando la validación validates_absence_of falla, un mensaje de error es almacenado en el objeto modelo leído "attribute must be blank".

8.2.2 validates_acceptance_of

Muchas aplicaciones web tienen pantallas en las cuales el usuario es invitado a aceptar términos de servicio o algún concepto similar, que usualmente involucra el chequeo de un box. No se requiere ninguna columna de base de datos real que coincide con el atributo declarado en la validación. Cuando usted llama este método, creará atributos virtuales automáticamente para cada atributo nombrado que usted especifique. Yo veo esta validación como un tipo de azúcar sintáctica ya que es específica para la programación de aplicaciones web.

class Account < ActiveRecord::Base
  validates_acceptance_of :privacy_policy, :terms_of_service
end

Usted puede usar estas validaciones con o sin una columna booleana en la tabla que respalda su modelo. Un atributo transiente será creado si es necesario. Elija almacenar el valor en una base de datos sólo si usted necesita dar seguimiento de si el usuario aceptó los términos para auditoria u otras razones. Eso sí, no aceptando el término evitaría creación del registro, pero es bueno saber lo que se admite.

Cuando la validación validates_acceptance_of falla, un mensaje de error el almacenado en el objeto modelo "attribute must be accepted".

La opción :accepted permite de manera simple cambiar el valor que se considera aceptado. El valor por defecto es "1", lo cual coincide con el valor entregado por un checkbox generado usando los métodos helpers de Rails.

class Cancellation < ActiveRecord::Base
  validates_acceptance_of :account_cancellation, accept: 'YES'
end

Si usted usa el ejemplo precedente en conjunto con el campo de texto conectado al atributo account_cancellation, el usuario tendrá que tipear la palabra YES con el fin de que el objeto cancellación sea válido.

8.2.3 validates_asociated

Usado para asegurarse que todos los objetos asociados son válidos al grabar. Trabaja con cualquier tipo de asociación y es específica de Active Records (no para Active Model). Enfatizamos el todos porque el comportamiento por defecto de las asociaciones has_many es asegurar la validez de los nuevos registros hijos al grabar.

Sugerencia: usted probablemente no necesita esta validación particular hoy en día ya que las asociaciones has_many por defecto tienen validate: true. Adicionalmente, note que una de las implicancias es que setear validate: true sin cuidado en una asociación belongs_to puede causar problemas de loops infinitos.

Un validates_associated sobre belongs_to no fallará si la asociación es nil. Si usted necesita asegurarse de que la asociación está llena y válida, usted debe usar validates_associated en conjunto con validates_presence_of.

Es posible obtener un comportamiento similar usando una combinación de las opciones :autosave y :validate sobre un has_many.

8.2.4 validates_confirmation_of

El método `validates_confirmation_of es otro caso de azucar sintáctica para aplicaciones web, ya que es común incluir ingresos duales de campos de texto para asegurarnos que el usuario ingrese correctamente datos críticos como la password o la dirección de email. Esta validación creará un atributo virtual para el valor de confirmación y compara los dos atributos para asegurarse que ellos coincidan con el propósito de que el modelo sea válido.

Aquí un ejemplo, usando nuestro modelo ficticio Account nuevamente:

class Account < ActiveRecord::Base
  validates_confirmation_of :password
end

La interface de usuario usada para setear los valores del modelo Account necesitará incluir campos de texto extra nombrados con el sufijo _confirmation, y cuando sea enviado, el valor de estos campos deberá coincidir para pasar la validación. Un ejemplo simplificado del código de la coincidencia en la vista es provisto.

= form_for account do |f|
  = f,label :login
  = f.text_field : login
  = f.label :password
  = f.password_field :password
  = f.label :password_confirmation
  = f.password_field :password_confirmation
  = f.submit

8.2.5 validates_each

El método validates_each es una forma un poco más libre que sus compañeros en la familia de validaciones en el sentido que no tiene una función de validación predefinida. A cambio, usted le entrega un arreglo de nombres de atributos para chequear y proporciona un block Ruby para ser usado en el chequeo de la validez de cada atributo. Note que los parámetros para la instancia del modelo (record), el nombre del atributo como un símbolo, y el valor a chequear son pasados como parámetro del block. La función block designa el objeto modelo como válido o no en la medida que agrega a su arreglo errors o no. El valor que retorna el block es ignorado.

NO hay muchas situaciones donde este método es necesario, pero un ejemplo plausible es cuando interactuamos con servicios externos para la validación. Usted puede envolver la validación externa en una fachada específica para su aplicación y entonces llamarla usando un block validates_each.

class Invoice < ActiveRecord::Base
  validates_each :supplier_id, :purchase_order do |record, attr, value|
    record.errors.add(attr) unless PurchasingSystem.validate(attr, value)
  end
end

8.2.6 validates_format_of

Para usar validates_format_of, usted debe conocer como usar expresiones regulares de Ruby (http://rubular.com). Pase al método uno o más atributos para chequear y una expresión regular como la opción (requerida) :with. Un buen ejemplo, como se muestra en la documentación Rails, es chequear el formato de un email válido:

class Person < ActiveRecord::Base
  validates_format_of :email, with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/
end

Por cierto, este ejemplo no es un corrector de formato de dirección de correo electrónico RFC compilant. Si usted necesita validar direcciones de email, intente el plugin en http://github.com/spectator/validates_email

Las expresiones regulares son increíbles pero pueden ser muy complejas, particularmente cuando validan nombres de dominios o direcciones de correo. Usted puede usar #{} dentro de una expresión regular, así divide su expresión regular en pedazos como este:

validates_format_of :name, with: /\A((localhost)|#{DOMAIN}|#{NUMERIC_IP})#{PORT}\z/

Esta expresión en más directa y fácil de comprender. Las constantes mismas no son fáciles de entender pero más simples de si ellas fueran enredadas juntas:

PORT = /(([:]\d+)?)/
DOMAIN = (/[a-z0-9\-]+\.?)*([a-z0-9]{2,})\.[a.z]{2,}/
NUMERIC_IP = /(?>(?:1?\d?\d|2[0-4]\d|25[0-5])\.){3}(?:1?\d?\d|2[0-4]\d|25[0-5])(?:\/(?:[12]?\d|3[012])|-(?:1\d?\d|2[0-4]\d|25[0-5])\.){3}(?:1?\d?\d|2[0-4]\d|25[0-5]))?/

Voy a tomar tu legibilidad y levantar su prueba de aislamiento. Tu expresión regular misma es una constante así que se puede probar

8.2.7 validates_inclusion_of y validates_exclusion_of

Estos métodos toman un número variable de nombres de atributos y una opción :in. Cuando se ejecutan, chequean que el valor del atributo está incluido (o excluido respectivamente) en el objeto enumerable pasado como la opción :in.

Los ejemplos en la documentación Rails son probablemente algunas de las mejores ilustraciones de su uso, así que me inspiraré en ellos:

class Person < ActiveRecord::Base
  validates_inclusion_of :gender, in: %w( m f ), message: 'O RLY?'
  ...

class Acoount < ActiveRecord::Base
  validates_exclusion_of :username, in: %w( admin superuser ), message: 'Borat says "Naughty, naughty!"'
  ...

Note que en los ejemplos, he introducido el uso de la opción :message, común a todos los métodos de validación, para personalizar el mensaje de error construido y agregado a la colección Errors cuando la validación falla. Cubriremos el mensaje de error por defecto y cómo personalizarlo después en este capítulo.

8.2.8 validates_length_of

El método validates_length_of toma una variedad de diferentes opciones y le permite especificar restricciones de largo para un atributo dado de su modelo.

class Account < ActiveRecord::Base
  validates_length_of :login, minimum: 5
end

** 8.2.8.1 Opciones de restricción**

Las opciones :minimum y :maximum trabajan como se espera, pero no las use juntas. Para especificar un rango, use la opción :within y pase un rango Ruby, como en los siguientes ejemplos:

class Account < ActiveRecord::Base
  validates_length_of :username, within: 5..20
end

Para especificar el largo exacto use la opción :is:

class Account < ActiveRecord::Base
  validates_length_of :account_number, is: 16
end

** 8.2.8.2 Opciones de mensaje de error**

Rails le da la habilidad de generar un mensaje de error detallado para validates_length_of vía las opciones :too_long, :too_short y wrong_length. Use %{count} en su mensaje de error personalizado como un placeholder para el número correspondiente a la restricción.

class Account < ActiveRecord::Base
  validates_length_of :account_number, is: 16, wrong_length: "Should be %{count} charecters long"
end

8.2.9 validates_numericality_of

El método torpemente llamado validates_numericality_of es usado para asegurarnos que un atributo puede tener sólo un valor numérico.

La opción :only_integer le permite especificar adicionalmente que el valor debe ser un valor entero y por defecto es falso.

class Account < ActiveRecord::Base
  validates_numericality_of :account_number, only_integer: true
end

Las opciones :even y odd hacen lo que usted espera y son útiles para cosas como, no sé, chequear la valencia de electrones (En realidad no soy lo suficientemente creativo para pensar como puede usar esta validación, pero usted podrá).

Las siguientes opciones de comparación también están disponibles:

  • :equal_to
  • :greater_than
  • :greater_than_or_equal_to
  • :less_than
  • :less_than_or_equal_to
  • :other_than

8.2.9.1 Infinito y otros valores especiales float

Interesante, Ruby tiene el concepto de infinito empaquetado. Si usted no ha visto el infinito antes, intente lo siguiente en la consola:

>> (1.0/0.0)
=> Infinity

Infinity es considerado un numero para validates_numericality_of. Las bases de datos (como PostgreSQL) que soportan el estándar IEEE 754 pueden permitir que valores especiales de float como Infinity puedan ser almacenados. Los otros valores especiales son el infinito positivo (-INF), infinito negativo (-INF), y no-número (NaN). IEEE 754 también distingue entre el cero positivo (+0) y el cero negativo (-0). NaN es usado para representar resultados de operaciones indefinidos.

8.2.10 validates_presence_of

Uno de los métodos de validación más comunes, validates_presence_of es usado para denotar atributos obligatorios. Este método chequea si un atributo está en blanco usando el método blank?, definido sobre Object, el cual retorna true para valores que son nil o strings en blanco ("").

class Account < ActiveRecord::Base
  validates_presence_of :username, :email, :account_number
end

Un error común es usar validates_presence_of con un atributo booleano, como el campo que respalda un check box. Si usted desea asegurarse que el atributo es true, use validates_acceptace_of. El valor booleano false es considerado en blanco, así si usted desea asegurarse de que sólo valores true o false son seteados en su modelo, use el siguiente patrón:

validates_inclusion_of :protected, in: [true, false]

8.2.10.1 Validando la presencia y/o existencia de objetos asociados

Cuando usted esta tratando de asegurarse que una asociación esta presente, pase validates_presence_of a su atributo llave externa, no a la variable de asociación misma. Note que la asociación puede fallar en casos donde tanto el padre como el hijo están sin grabar (ya que la llave externa estará en blanco).

Muchos desarrolladores tratan de usar esta validación con la intención de asegurarse que los objetos asociados existan realmente en la base de datos. Personalmente, pienso que puede ser un caso de uso válido para una restricción de llave externa actual en la base de datos, pero si usted desea hacer el chequeo en su código Rails, entonces emule el siguiente ejemplo:

class Timesheet < ActiveRecord::Base
  belongs_to :user
  validates_presence_of :user_id
  validate :user_exists

  protected

  def user_exists
    errors.add(:user_id, "doesn't exist") unless User.exists?(user_id)
  end
end

Sin una validación, si su aplicación viola una restricción de llave externa, usted puede obtener una excepción de Active Record.

8.2.11 validates_uniqueness_of

El método validates_uniqueness_of, también exclusivo de Active Record, se asegura que el valor de un atributo es único para todos los modelos del mismo tipo. Esta validación no trabaja al agregar una restricción de unicidad al nivel de la base de datos. Trabaja construyendo y ejecutando una consulta por registros coincidentes en la base de datos. Si cualquier registro es retornado cuando este método hace la consulta, la evaluación falla.

class Account < ActiveRecord::Base
  validates_uniqueness_of :username
end

Al especificar una opción :scope, atributos adicionales pueden ser usados para determinar la unicidad. Usted puede pasar a :scope uno o más nombres de atributos como símbolos (poniendo múltiples símbolos en un arreglo).

class Address < ActiveRecord::Base
  validates_uniqueness_of :line_two, scope: [:line_one, :city, :zip]
end

También es posible especificar si hacemos una restricción de unicidad con sensibilidad a las mayúsculas vía la opción case_sensitive (ignorada para atributos no texto).

Con la inclusión del soporte para arreglos de columnas de PostgreSQL en Rails 4, el método validates_uniqueness_of puede ser usado para validar que todos los items en ese arreglo son únicos. Los arreglos de columnas de PostgreSQL son cubiertos en detalle en el Capítulo 9 "Active Record Avanzado".

Esta validación no es a prueba de tontos, debido a una potencial competencia entre la consulta SELECT que chequea los duplicados y los INSERT o UPDATE que almacenan el registro. Una excepción de Active Record puede ser generada como un resultado, así que prepárese para manejar esta falla en su controlador. Yo recomiendo que usted use una restricción de índice único en la base de datos si usted debe estar absolutamente seguro de que el valor de la columna es único.

8.2.11.1 Obligando la unicidad de modelos join

En la medida que usted use modelos join (con has_many :through), es muy común necesitar que la relación sea única. Considere una aplicación que modelo students, courses y resgistrations con el código siguiente:

class Student < ActiveRecord::Base
  has_many :resgistrations
  has_many :courses, through: :registrations
end

class Registration < ActiveRecord::Base
  belongs_to :student
  belongs_to :course
end

class Course < ActiveRecord::Base
  has_many :registrations
  has_many :students, through: :resgistrations
end

Como asegurarnos de que un usuario no se registre más de una vez a un curso particular? La forma más concisa es usar validates_uniqueness_of con una restricción :scope. Lo importante de recordar con esta técnica es referenciar la llave externa, no los nombres de las asociaciones:

class Registration < ActiveRecord::Base
  belongs_to :student
  belongs_to :course

  validates_uniqueness_of :student_id, scope: course_id, message: "can only register once per course"
end

Note que ya que el mensaje de error por defecto generado cuando esta validación falla podría no tener sentido, se puede usar "Student can only register once per course."

Los lectores astutos notarán que la validación fue sobre student_id pero el mensaje de error referencia a "Students".

8.2.11.2 Limitar la vista de la restricción

A partir de Rails 4, uno puede especificar criterios que restrinjan una validación de unicidad contra un conjunto de registros setando la opción :conditions.

Para ilustrar, asumamos que tenemos un artículo que requiere títulos que deben ser únicos contra todos los artículos publicados en una base de datos. Podemos lograr esto usando validates_uniqueness_of al hacer lo siguiente:

class Article < ActiveRecord::Base
  validates_uniqueness_of :title, conditions: -> { where.not(published_at: nil) }
  ...
end

Cuando el modelo es grabado, Active Record consultará por el título contra todos los artículos en la base de datos que son publicados. Si no se retornan resultados, el modelo es válido.

8.2.12 validates_with

Todos los métodos de validación que hemos discutido hasta el momento son esencialmente locales a la clase en la cual son usados. Si usted desea desarrollar una suite de clases de validación personalizadas y reusables, entonces usted necesita una forma de aplicarlos a sus modelos, y eso es lo que el método validates_with le permite hacer.

Para implementar un validador personalizado, extender ActiveRecord::Validator e implemente el método validate. El registro será validado si está disponible como record, y usted manipula su hash errors para hacer el log de la validación.

Los ejemplos siguientes, desde el excelente blog de Ryan Daigle sobre esta característica (http://ryandaigle.com/articles/2009/8/11/what-s-new-in-edge-rails-independent-model-validators), muestra un validador de campo email reusable:

class EmailValidator < ActiveRecord::Validator
  def validate()
    record.errors[:email] << "is not valid" unless record.email =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/
  end
end

class Account < ActiveRecord::Base
  validates_with EmailValidator
end

El ejemplo asume la existencia de un atributo email en el registro. Si usted necesita hacer su validador reusable más flaxible, usted puede acceder la opción de validación en tiempo de ejecución vía el hash options como el siguiente código:

class EmailValidator < ActiveRecord::Validator
  def validate()
    email_field = options[:attr]
    record.errors[email_field] << "is not valid" unless record.send(email_field) =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/
  end
end

class Account < ActiveRecord::Base
  validates_with EmailValidator, attr: :email
end

8.2.13 RecordInvalid

Donde quiera que usted realice una operación bang (como save!) y la validación falla, usted debe estar preparado para rescatar ActiveRecord::RecordInvalid. Las fallas de la validación pueden causar que RecordInvalid sea levantado y su mensaje contendrá una descripción de la falla.

Aquí un pequeño ejemplo desde una de mis aplicaciones que tiene validaciones restrictivas sobre el modelo User.

>> u = User.new
=> #<User ...>
>> u.save!
ActiveRecord::RecordInvalid: Validation failed: Name can't be blank,
password confirmation can't be blank, Password is too short (nimimum
is 5 characters), Email can't be blank, Email address format is bad

8.3 Opciones de validación comunes

Las siguientes opciones aplican a todos los métodos de validación.

8.3.1 :allow_blank y :allow_nil

En algunos casos, usted sólo necesita gatillar una validación si el valor está presente -en otras palabras, el atributo es opcional. Hay dos opciones que proveen esta funcionalidad.

La opción :allow_blank salta la validación si el valor es blank de acuerdo al método blank?. Similarmente, la opción :allow_nil salta la validación si el valor del atributo es nil; sólo chequea nil, los strings vacíos no estan considerados nil -son considerados blank.

8.3.2 :if y :unless

Las opciones :if y :unless son cubiertas en la siguiente sección, "Validaciones condicionales".

8.3.3 :message

Como hemos discutido anteriormente en el capítulo, la forma en que el proceso de validación registra fallas es agregando ítems a la colección Errors del objeto modelo que está siendo chequeado. Parte del ítem error es un mensaje específico que describe la falla de validación. Todos los métodos de validación aceptan la opción :message para que usted pueda sobre-escribir el formato del mensaje de error por defecto.

class Account < ActiveRecord::Base
  validates_uniqueness_of :username, message: "is already taken"
end

El archivo local de English en ActiveModel define la mayoría de los templates de mensajes de error estándar.

inclusion: "is not inlcuded in the list"
exclusion: "is reserved"
invalid: "in invalid"
confirmation: "doesn't match %{attribute}"
accepted: "must be accepted"
empty: "can't be empty"
blank: "can't be blank"
present: "must be blank"
too_long: "is too long (maximum is %{count} characters)"
too_short: "is too short (minimum is %{count} characters)"
wrong_length: "is the wrong length (should be %{count} characters)"
not_a_number: "is not a number"
not_an_integer: "is not an integer"
greater_than: "must be greater than %{count}"
greater_than_or_equal_to: "must be grater than or equal to &{count}"
equal_to: "must be equal to %{count}"
less_than: "must be less than &{count}"
less_than_or_equal_to: "must be less than or equal to %{count}"
other_than: "must be other than %{count}"
odd: "must be odd"
even: "must be even"

El mensaje por defecto sólo usa la variable count para interpolaciones, donde es apropiado, pero model, attribute y value están siempre disponibles.

validates_uniqueness_of :username, message: "%{value} is already registered"

8.3.4 :on

Por defecto, las validaciones corren al grabar (create y update). Si usted necesita, puede limitar una validación dada a sólo una de esas operaciones pasando una opción :on con :create o :update.

Asumiendo que su aplicación no soporta cambios de email, un buen uso para on: :create puede ser en conjunto con validates_uniqueness_of, ya que la consulta de unicidad en bases de datos grandes puede consumir mucho tiempo.

class Account < ActiveRecord::Base
  validates_uniqueness_of :email, on: :create
end

8.3.5 :strict

Nueva en Rails 4 es la opción de validación :strict. Seteando :strict en true causa que una excepción -ActiveModel::StrictValidationFailed- sea levantada cuando el modelo no es válido.

class Account < ActiveRecord::Base
  validates :email, presence: { strict: true }
end

Para sobre-escribir el tipo de excepción levantada sobre el error, pase la excepción personalizada a la opción :strict.

8.4 Validación condicional

Ya que todos los métodos de validación son implementados vía el API de Callbacks de Active Model, ellos aceptan también las opciones :if y :unless para determinar en tiempo de ejecución (y no durante la definición de clase) si la validación necesita ser ejecutada o no. Los siguientes tres tipos de argumentos pueden ser entregados como opciones :if y :unless:

Simbolo El nombre de un método a invocar como un símbolo. Esta es probablemente la opción más común y ofrece el mejor desempeño.

String* Un retazo de código Ruby a eval puede ser útil cuando la condición es realmente muy corta, pero tenga en mente que evaluar instrucciones es relativamente más lento.

Proc Un bloque de código para ser evaluado con instance_eval, de modo que self es el registro actual. Quizá la opción más elegante para condicionales de una línea.

validates_presence_of :approver, if: -> { approved? && !legacy? }

8.4.1 Uso y consideraciones

Cuando tiene sentido usar validaciones condicionales? La respuesta es cuando un objeto pueda ser válidamente registrado en más de un estado. Un ejemplo muy común involucra el modelo User (o Person), usado para el login y autenticación.

validates_presence_of :password, if: :password_requiered?
validates_presence_of : password_confirmation, if: :password_required?
validates_length_of :password, within 4..40, if: :password_required?
validates_confirmation_of :password, if: :password_required?

Este código no es DRY (es repetitivo). Usted puede refactorizarlo para hacerlo un poco más dry usando el método with_options que Rails mezcla dentro del objeto.

with_options if: password_required? do |user|
  user.validates_presence_of :password
  user.validates_presence_of :password_confirmation
  user.validates_length_of :password, within: 4..40
  user.validates_confirmation_of :password
end

Todas las validaciones ejemplo chequean los dos casos cuando un campo password (texto plano) debe ser requerido con el fin de que el modelo sea válido.

def password_requiered?
  encrypted_password.blank? || !password.blank?
end

El primer caso es si el atributo encrypted_password es blank, porque eso significa que estamos tratando con una nueva instancia de User a la que aún no se le ha dado una password. El otro caso es cuando el atributo password no es blank; quizá esto pasa durante la actualización y el usuario intenta volver a setear su propia password.

8.4.2 Contextos de validación

Otra forma de generar validaciones condicionales esta soportadas por los contextos de validación. Declare una validación y pase el nombre de un contexto de validación específico de la aplicación como valor de la opción :on. Esta validación puede ahora ser sólo chequeada cuando es explícitamente invocada usando `record.valid?(context_name).

Considere el ejemplo siguiente que involucra una aplicación de generación de reportes. Grabar un reporte sin nombre está bien pero publicar uno sin nombre no.

class Report < ActiveReport::Base
  validates_presence_of :name, on: : publish
end

class ReportsController < ApplicationController
  expose(:report)

  # POST /reports/1/publish
  def publish
    if report.valid? :publish
      redirect_to report, notice: "Report published"
    else
      flash.now.alert = "Can't publish unnamed reports!"
      render :show
    end
  end
end

8.5 Validaciones abreviadas

Introducido en Rails 3, el método validates identifica un atributo y acepta opciones que corresponden a las que hemos cubierto en el capítulo. Usando validates puede apretar el código de su modelo simpáticamente.

validates :username, presence: true,
  format: { with: /[A-Za-z0-9]+/ },
  length: { minimum: 3 },
  uniqueness: true

Las siguientes opciones están disponibles para usar con el método validates.

absence: true Alias para validates_absence_of. Entrega opciones adicionales al reemplazar true con un hash.

validates :unwanted, absence: { message: "You shouldn't have set that" }

acceptance: true Alias para validates_acceptance_of, usado tipicamente con check boxes que indican la aceptación de términos. Entrega opciones adicionales al reemplazar true por un hash.

validates :terms, acceptance: { message: "You must accept terms." }

confirmation: true Alias para validates_confirmation_of, usado típicamente para asegurarnos que los campos de confirmación email y password coincidan correctamente. Entrega opciones adicionales al reeplazar true por un hash.

validates :email, confirmation: { message: 'Try again.' }

exclusion: { in: [1,2,3] } Alias para validates_exclusion_of. Si su única opción es el arreglo contra el cual se excluye, usted puede acortar la sintaxis entregando un arreglo como el valor.

validates :username, exclusion: %w(admin superuser)

format: { with: /.*/ } Alias para validates_format_of. Si su única opción es la expresión regular, usted puede acortar la sintaxis al valor como lo siguiente:

format: /[A-Za-z0-9]+/

inclusion: { in: [1,2,3] } Alias para validates_inclusion_of. Si su única opción es el arreglo contra lo cual se incluye, usted puede acortar la sintaxis hasta el arreglo de valores.

validates :gender, inclusion: %w(male female)

length: { minimum: 0, maximum: 1000} Alias para validates_length_of. Si su únicas opciones son el mínimo y el máximo, usted puede acortar la sintaxis entregando un rango de valores Ruby.

validates :username, length: 3..20

numericality: true Alias para validates_numericality_of. Entrega opciones adicionales reemplazando true por un hash.

validates :quantity, numericality: { message: 'Supply a number.' }

presence: true Alias para validates_presence_of. Entrega opciones adicionales al reemplazar true por un hash.

validates :username, presence: { message: 'How do you expect to login?' }

uniqueness: true Alias para validates_uniqueness_of. Entrega opciones adicionales al reemplazar true por un hash.

validates :quantity, uniqueness: { message: "You're SOL on that login choice, buddy!" }

8.6 Técnicas de validación personalizada

Cuando las macros de validación declarativas no son suficientes para su aplicación, Rails le da algunas técnicas personalizadas.

8.6.1 Agregar macros de validación personalizada a su aplicación

Rails tiene la habilidad de agregar macros de validación personalizada (disponible para todas sus clases de modelos) extendiendo ActiveModel::EachValidator.

El siguiente ejemplo es tonto pero mustra claramente la funcionalidad.

class ReportLikeValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless value["Report"]
      record.errors.add(attribute, 'does not appear to be a Report')
    end
  end
end

Ahora que su validación personalizada existe, esta disponible para ser usada con la macro validates en su modelo.

class Report < ActiveRecord::Base
  validates :name, report_like: true
end

La llave report_like es inferida desde el nombre de la clase del validador, el cual en este caso es ReportLikeValidator.

Usted puede recibir opciones vía el método validates agregando un método initializer a su clase de validador personalizado. Por ejemplo, hagamos que ReportLikeValidator más genérico.

class LikeValidator < ActiveModel::EachValidator
  def initializer(options)
    @with = options[:with]
    super
  end

  def validate_each(record, attribute, value)
    unless value[@with]
      record.errors.add(attribute, "does not appear to be like #{@with}")
    end
  end
end

Nuestro código de modelo podría cambiar a lo siguiente:

class Report < ActiveRecord::Base
  validates :name, like: { with: "Report" }
end

8.6.2 Crear una clase de validador personalizada

Esta técnica involucra la herencia desde ActiveModel::Validator y la implementación de un método validate que toma el registro a validar.

Lo mostraré con un ejemplo realmente malvado.

class RandomlyValidator < ActiveModel::Validator
  def validate(record)
    record.errors[:base] << "FAIL #1" unless first_hurdle(record)
    record.errors[:base] << "FAIL #2" unless second_hurdle(record)
    record.errors[:base] << "FAIL #3" unless third_hurdle(record)
  end

  private

  def first_hurdle(record)
    rand > 0.3
  end

  def second_hurdle(record)
    rand > 0.6
  end

  def third_hurdle(record)
    rand > 0.9
  end

Use su validador personalizado en un modelo con la macro validates_with.

class Report < ActiveRecord::Base
  validates_with RandomlyValidator
end

8.6.3 Agregar un método validate en su modelo

Un método de instancia validate puede ser una forma de chequear el estado de su objeto holísticamente y poner el código para hacerlo dentro de la clase modelo misma. (Esta es una técnica antigua que no puedo endosar completamente, agrega complejidad a su clase de modelo innecesariamente, dado lo fácil que es crear una clase de validador personalizado).

Por ejemplo, asuma que usted está tratando con un objeto modelo con un conjunto de tres atributos enteros (:attr1, :attr2, y :attr3) y un atributo total precalculado. El total debe ser siempre igual a la suma de los tres atributos:

class CompletelyLameTotalExample < ActiveRecord::Base
  def validate
    if total != (attr1 + attr2 + attr3)
      errors[:total] << "The total doesn't add up!"
    end
  end
end

Usted puede alternativamente agregar un mensaje de error al objeto completo en lugar de sólo a un atributo particular usando la llave :base, como a continuación:

errors[:base] << "The total doesn't add up!"

Recuerde, la forma de marcar un objeto como no válido es agregar a su objeto Errors. El valor retornado de un método de validación personalizado no es usado.

8.7 Saltandose las validaciones

Los métodos update_attribute y update_column no invocan validaciones, aunque su método acompañante update si lo hace. Quienquiera que haya escrito los dosumentos de las API piensa que este comportamiento es "especialmente útil para banderas booleanas sobre registros existentes".

Yo no sé si esto es verdad o no, pero sí sé que es fuente de discordia en la comunidad. Desafortunadamente, no tengo mucho más que agregar más que un simple consejo de sentido común. Sea muy cuidadoso al usar los métodos update_attribute o update_column. Pueden llevar sus modelos fácilmente a estados no válidos.

8.8 Trabajando con el hash de errores

Algunos métodos son provistos para permitirle agregar errores de validación a la colección manualmente y alterar el estado del hash Errors.

8.8.0.1 errors[:base] = msg

Agrega un mensaje de error relacionado con el estado del objeto en su totalidad y no con el valor de un atributo en particular. Construya sus mensajes de error con frases completas, porque Rails no hace ningún proceso adicional para hacerlas más legibles.

8.8.0.2 errors[:attribute] = msg

Agrega un mensaje de error relacionado con un atributo en particular. El mensaje debe ser un fragmento de frase que se lea naturalmente cuando se anteponga al nombre en mayúsculas del atributo.

8.8.0.3 clear

Como usted puede imaginar, el método clear limpia la colección Errors.

8.8.1 Chequeando por errores

También es posible chequear el objeto Errors por fallas de validación en atributos específicos con sólo usar la notación de paréntesis cuadrados. Un arreglo es siempre retornado, pero estará vacío si no hay errores de validación para el atributo especificado.

>> user.errors[:login]
=> ["Zed is already registered"]
>> user.errors[:password]
=> []

Alternativamente, uno puede accesar los mensajes de error completos para un atributo específico usando el método full_messages_for, como si estuviera accesando fallas de validación para atributos con la notación de paréntesis cuadrados, un arreglo será siempre retornado.

>> user.errors.full_messages_for(:email)
=> ["Email can't be blank"]

8.9 Probando validaciones con Shoulda

A pesar de que las validaciones son código declarativo, si usted está haciendo TDD, entonces usted deberá especificarlas antes de escribirlas. Afortunadamente, la librería Shoulda Matchers de Thoughtbot contiene un conjunto de matchers diseñados para probar fácilemente validaciones.

describe Post do
  it { should validate_uniqueness_of(:title) }
  it { should validate_presence_of(:body).with_message(/wtf/) }
  it { should validate_presence_of(:title) }
  it { should validate_numericality_of(:user_id) }
end

describe User do
  it { should_not allow_value("blah").for(:email) }
  it { should_not allow_value("b lah").for(:email) }
  it { should allow_value("[email protected]").for(:email) }
  it { should allow_value("[email protected]").for(:email) }
  it { should ensure_length_of(:email)is_at_least(1).is_at_most(100) }
  it { should ensure_inclusion_of(:age).in_range(1..100) }
end
⚠️ **GitHub.com Fallback** ⚠️