Rails源码研究之ActiveRecord:四,Validations

Validations相关的源码全在validations.rb文件里:
[code]
module ActiveRecord
class Errors
include Enumerable

@@default_error_messages = {
:inclusion => "is not included in the list",
:exclusion => "is reserved",
:invalid => "is invalid",
:confirmation => "doesn't match confirmation",
:accepted => "must be accepted",
:empty => "can't be empty",
:blank => "can't be blank",
:too_long => "is too long (maximum is %d characters)",
:too_short => "is too short (minimum is %d characters)",
:wrong_length => "is the wrong length (should be %d characters)",
:taken => "has already been taken",
:not_a_number => "is not a number"
}

def add(attribute, msg = @@default_error_messages[:invalid])
@errors[attribute.to_s] = [] if @errors[attribute.to_s].nil?
@errors[attribute.to_s] << msg
end

def add_on_empty(attributes, msg = @@default_error_messages[:empty])
for attr in [attributes].flatten
value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
is_empty = value.respond_to?("empty?") ? value.empty? : false
add(attr, msg) unless !value.nil? && !is_empty
end
end

end

module Validations
def self.included(base) # :nodoc:
base.extend ClassMethods
base.class_eval do
alias_method_chain :save, :validation
alias_method_chain :save!, :validation
alias_method_chain :update_attribute, :validation_skipping
end
end

module ClassMethods

def validate(*methods, &block)
methods << block if block_given?
write_inheritable_set(:validate, methods)
end

def validate_on_create(*methods, &block)
methods << block if block_given?
write_inheritable_set(:validate_on_create, methods)
end

def validate_on_update(*methods, &block)
methods << block if block_given?
write_inheritable_set(:validate_on_update, methods)
end

def validates_each(*attrs)
options = attrs.last.is_a?(Hash) ? attrs.pop.symbolize_keys : {}
attrs = attrs.flatten

# Declare the validation.
send(validation_method(options[:on] || :save)) do |record|
# Don't validate when there is an :if condition and that condition is false
unless options[:if] && !evaluate_condition(options[:if], record)
attrs.each do |attr|
value = record.send(attr)
next if value.nil? && options[:allow_nil]
yield record, attr, value
end
end
end
end

def validates_confirmation_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:confirmation], :on => :save }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)

attr_accessor *(attr_names.map { |n| "#{n}_confirmation" })

validates_each(attr_names, configuration) do |record, attr_name, value|
record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation")
end
end

def validates_presence_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:blank], :on => :save }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)

# can't use validates_each here, because it cannot cope with nonexistent attributes,
# while errors.add_on_empty can
attr_names.each do |attr_name|
send(validation_method(configuration[:on])) do |record|
unless configuration[:if] and not evaluate_condition(configuration[:if], record)
record.errors.add_on_blank(attr_name,configuration[:message])
end
end
end
end

def validates_length_of(*attrs)
# Merge given options with defaults.
options = {
:too_long => ActiveRecord::Errors.default_error_messages[:too_long],
:too_short => ActiveRecord::Errors.default_error_messages[:too_short],
:wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length]
}.merge(DEFAULT_VALIDATION_OPTIONS)
options.update(attrs.pop.symbolize_keys) if attrs.last.is_a?(Hash)

# Ensure that one and only one range option is specified.
range_options = ALL_RANGE_OPTIONS & options.keys
case range_options.size
when 0
raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.'
when 1
# Valid number of options; do nothing.
else
raise ArgumentError, 'Too many range options specified. Choose only one.'
end

# Get range option and value.
option = range_options.first
option_value = options[range_options.first]

case option
when :within, :in
raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range)

too_short = options[:too_short] % option_value.begin
too_long = options[:too_long] % option_value.end

validates_each(attrs, options) do |record, attr, value|
if value.nil? or value.split(//).size < option_value.begin
record.errors.add(attr, too_short)
elsif value.split(//).size > option_value.end
record.errors.add(attr, too_long)
end
end
when :is, :minimum, :maximum
raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0

# Declare different validations per option.
validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" }
message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }

message = (options[:message] || options[message_options[option]]) % option_value

validates_each(attrs, options) do |record, attr, value|
if value.kind_of?(String)
record.errors.add(attr, message) unless !value.nil? and value.split(//).size.method(validity_checks[option])[option_value]
else
record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value]
end
end
end
end

alias_method :validates_size_of, :validates_length_of

def validates_uniqueness_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken], :case_sensitive => true }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)

validates_each(attr_names,configuration) do |record, attr_name, value|
if value.nil? || (configuration[:case_sensitive] || !columns_hash[attr_name.to_s].text?)
condition_sql = "#{record.class.table_name}.#{attr_name} #{attribute_condition(value)}"
condition_params = [value]
else
condition_sql = "LOWER(#{record.class.table_name}.#{attr_name}) #{attribute_condition(value)}"
condition_params = [value.downcase]
end
if scope = configuration[:scope]
Array(scope).map do |scope_item|
scope_value = record.send(scope_item)
condition_sql << " AND #{record.class.table_name}.#{scope_item} #{attribute_condition(scope_value)}"
condition_params << scope_value
end
end
unless record.new_record?
condition_sql << " AND #{record.class.table_name}.#{record.class.primary_key} <> ?"
condition_params << record.send(:id)
end
if record.class.find(:first, :conditions => [condition_sql, *condition_params])
record.errors.add(attr_name, configuration[:message])
end
end
end

def validates_format_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)

raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp)

validates_each(attr_names, configuration) do |record, attr_name, value|
record.errors.add(attr_name, configuration[:message]) unless value.to_s =~ configuration[:with]
end
end

def validates_numericality_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:not_a_number], :on => :save,
:only_integer => false, :allow_nil => false }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)

if configuration[:only_integer]
validates_each(attr_names,configuration) do |record, attr_name,value|
record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_before_type_cast").to_s =~ /\A[+-]?\d+\Z/
end
else
validates_each(attr_names,configuration) do |record, attr_name,value|
next if configuration[:allow_nil] and record.send("#{attr_name}_before_type_cast").nil?
begin
Kernel.Float(record.send("#{attr_name}_before_type_cast").to_s)
rescue ArgumentError, TypeError
record.errors.add(attr_name, configuration[:message])
end
end
end
end
end

def valid?
errors.clear

run_validations(:validate)
validate

if new_record?
run_validations(:validate_on_create)
validate_on_create
else
run_validations(:validate_on_update)
validate_on_update
end

errors.empty?
end

def errors
@errors ||= Errors.new(self)
end
end
end
[/code]
从上述代码我们可以看出以下几点:
1,save、save!方法会使用validation,而update_attribute则skip validation
2,record.errors方法得到该record经过validate后的所有errors
3,save(false)会skip validation

overwriting Base#validate的例子:
[code]
class Person < ActiveRecord::Base
protected
def validate
errors.add_on_empty %w( first_name last_name )
errors.add("phone_number", "has invalid format") unless phone_number =~ /[0-9]*/
end

def validate_on_create # is only run the first time a new object is saved
unless valid_discount?(membership_discount)
errors.add("membership_discount", "has expired")
end
end

def validate_on_update
errors.add_to_base("No changes have occurred") if unchanged_attributes?
end
end
[/code]

view中:
[code]
<% unless @post.errors.empty? %>
The post couldn't be saved due to these errors:
<ul>
<% @post.errors.each_full do |message| %>
<li><%= message %></li>
<% end %>
</ul>
<% end %>
[/code]
或者
[code]
<%= error_messages_for 'post' %>
[/code]

看了validations的代码,我们甚至可以写自己的Validation关键字:
[code]
module ActiveRecord
module Validations
module ClassMethods
def validates_oak_id(*attr_names)
configuration = { :message => "Invalid Oak ID" }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)

validates_each (attr_names, configuration) do |record, attr_name, value|
record.errors.add(attr_name, configuration[:message]) unless FAUtils.valid_oakid?(value)
end
end
end
end
end
[/code]
这个例子使用FAUtils库来验证oakid的合法性,一切都很简单吧?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值