# -*- encoding: utf-8 -*- require 'data_mapper/support/equalizer' require 'data_mapper/validation/message_transformer' require 'data_mapper/validation/violation' module DataMapper module Validation # TODO: rewrite this. specifically, validation logic should not # be intertwined with message generation. # Also, supporting multiple validation types per validator is too complicated class Rule extend Equalizer EQUALIZE_ON = [ :attribute_name, :allow_nil, :allow_blank, :custom_message, :if_clause, :unless_clause] equalize *EQUALIZE_ON # @api private attr_reader :attribute_name # @api private attr_reader :allow_nil # @api private attr_reader :allow_blank # @api private attr_reader :custom_message # @api private attr_reader :if_clause # @api private attr_reader :unless_clause # Get the validators for the given attribute_name and options # # @param [Symbol] attribute_name # the name of the attribute to which the returned validators will be bound # @param [Hash] options # the options with which to configure the returned validators # # @return [#each(Rule)] # a collection of validators which collectively # def self.rules_for(attribute_name, options, &block) Array(new(attribute_name, options, &block)) end # Construct a validator. Capture the :if and :unless clauses when # present. # # @param [String, Symbol] attribute_name # The name of the attribute to validate. # # TODO: remove Hash as a value for :message # (see Violation#[] in backwards.rb) # # @option [String, Hash] :message # A custom message that will be used for any violations of this rule # @option [Symbol, Proc] :if # The name of a method (on the valiated resource) or a Proc to call # (with the resource) to determine if the rule should be applied. # @option [Symbol, Proc] :unless # The name of a method (on the valiated resource) or a Proc to call # (with the resource) to determine if the rule should *not* be applied. # @option [Boolean] :allow_nil # Whether to skip applying this rule on nil values # @option [Boolean] :allow_blank # Whether to skip applying this rule on blank values def initialize(attribute_name, options = {}) @attribute_name = attribute_name @custom_message = options.fetch(:message, nil) @if_clause = options.fetch(:if, nil) @unless_clause = options.fetch(:unless, nil) @allow_nil = options[:allow_nil] if options.include?(:allow_nil) @allow_blank = options[:allow_blank] if options.include?(:allow_blank) end # Determines if this validator should be run against the # resource by evaluating the :if and :unless clauses # optionally passed while specifying any validator. # # @param [Object] resource # The resource that we check against. # # @return [Boolean] # true if should be run, otherwise false. # # @api private def execute?(resource) if if_clause = self.if_clause evaluate_conditional_clause(resource, if_clause) elsif unless_clause = self.unless_clause !evaluate_conditional_clause(resource, unless_clause) else true end end # Validate the +resource+ arg against this Rule # # @param [DataMapper::Resource, Object] resource # the target object to be validated # # @return [NilClass, Violation] # NilClass if +resource+ is valid # Violation with additional info if +resource+ is invalid def validate(resource) if valid?(resource) nil else Violation.new(resource, custom_message, self) end end # @api private def violation_info(resource) Hash[ violation_data(resource) ] end # @api private def violation_values(resource) violation_data(resource).map { |(_, value)| value } end # @api private def violation_data(resource) [ ] end # Add an error message to a resource. If the error corresponds to # a specific attribute name of the resource, add it to the errors for that # attribute name, otherwise add it under the :general attribute name # # @param [Object] resource # The resource that has the error. # @param [String] message # The message to add. # @param [Symbol] attribute_name # The name of the field that caused the error. # # @return [Rule] # The receiver (self) # # TODO: remove this method # Rules should return Violations, not mutate resource def add_error(resource, message, attribute_name = :general) resource.errors.add(attribute_name, message) self end def allow_nil? defined?(@allow_nil) ? @allow_nil : false end def allow_blank? defined?(@allow_blank) ? @allow_blank : false end # Test the value to see if it is blank or nil, and if it is allowed. # Note that allowing blank without explicitly denying nil allows nil # values, since nil.blank? is true. # # @param [Object] value # The value to test. # # @return [Boolean] # true if blank/nil is allowed, and the value is blank/nil. # # @api private def optional?(value) if value.nil? defined?(@allow_nil) ? allow_nil? : allow_blank? elsif DataMapper::Ext.blank?(value) allow_blank? end end def inspect out = "#<#{self.class.name}" # out << "@attribute_name=#{attribute_name.inspect} " # out << "@if_clause=#{if_clause.inspect} " if if_clause # out << "@unless_clause=#{unless_clause.inspect} " if unless_clause # out << "@options=#{options.inspect}>" self.class::EQUALIZE_ON.each do |ivar| value = send(ivar) out << " @#{ivar}=#{value.inspect}" end out << ">" end alias_method :to_s, :inspect private def allow_nil! @allow_nil = true end def allow_blank! @allow_blank = true end # @api private def evaluate_conditional_clause(resource, clause) if clause.kind_of?(Symbol) resource.__send__(clause) elsif clause.respond_to?(:call) clause.call(resource) end end # Get the corresponding Resource property, if it exists. # # Note: DataMapper validations can be used on non-DataMapper resources. # In such cases, the return value will be nil. # # @api private def get_resource_property(resource, property_name) model = resource.model if resource.respond_to?(:model) repository = resource.repository if model properties = model.properties(repository.name) if model properties[property_name] if properties end end # class Rule end # module Validation end # module DataMapper