require 'active_model'
require 'active_support/core_ext/array'
module ActiveModel
module Validations
class NestedValidator < EachValidator
private
def validate_each(record, attribute, values)
with_each_value(values) do |index, value|
prefix = prefix(attribute, index, include_index?(values))
record_error(record, prefix, value) if value.invalid?
end
end
def with_each_value(values, &block)
case values
when Hash
values.each { |key, value| block.call key, value }
else
Array.wrap(values).each_with_index { |value, index| block.call index, value}
end
end
def include_index?(values)
values.respond_to? :each
end
def prefix(attribute, index, include_index)
prefix = (options.has_key?(:prefix) ? options[:prefix] : attribute).to_s
prefix << "[#{index}]" if include_index
prefix
end
def record_error(record, prefix, value)
value.errors.each do |key, error|
record.errors.add(nested_key(prefix, key), error) if include?(key)
end
end
def nested_key(prefix, key)
"#{prefix} #{key}".strip.to_sym
end
def include?(key)
if options[:only]
only.any?{|k| key =~ /^#{k}/}
elsif options[:except]
except.none?{|k| key =~ /^#{k}/}
else
true
end
end
def only
@only ||= Array.wrap(options[:only])
end
def except
@except ||= Array.wrap(options[:except])
end
end
module HelperMethods
# Bases an object's validity on nested attr_names.
#
# class Parent < ActiveRecord::Base
# has_one :child
#
# validates_nested :child
# end
#
# class Child < ActiveRecord::Base
# has_one :child
#
# validates_presence_of :attribute
# end
#
# Any errors in the child will be copied to the parent using the child's name as
# a prefix for the error:
#
# puts parent.errors.messages #=> { :'child attribute' => ["can't be blank"] }
#
# @param attr_names attribute names followed with options
#
# @option attr_names [String] :prefix The prefix to use instead of the attribute name
#
# @option attr_names [String, Array] :only The name(s) of attr_names to include
# when validating. Default is all
#
# @option attr_names [String, Array] :except The name(s) of attr_names to exclude
# when validating. Default is none
#
# @option attr_names [Symbol] :on Specifies when this validation is active. Runs in all
# validation contexts by default (+nil+), other options are :create
# and :update.
#
# @option attr_names [Symbol, String or Proc] :if a method, proc or string to call to determine
# if the validation should occur (e.g. if: :allow_validation,
# or if: Proc.new { |user| user.signup_step > 2 }). The method,
# proc or string should return or evaluate to a true or false value.
#
# @option attr_names [[Symbol, String or Proc]] :unless a method, proc or string to call to determine
# if the validation should not occur (e.g. unless: :skip_validation,
# or unless: Proc.new { |user| user.signup_step <= 2 }). The method,
# proc or string should return or evaluate to a true or false value.
#
# @option attr_names [boolean] :strict Specifies whether validation should be strict.
# See ActiveModel::Validation#validates! for more information.
#
def validates_nested(*attr_names)
validates_with NestedValidator, _merge_attributes(attr_names)
end
end
end
end