lib/dao/validations/validator.rb in dao-4.2.1 vs lib/dao/validations/validator.rb in dao-4.4.2

- old
+ new

@@ -1,170 +1,275 @@ +# -*- encoding : utf-8 -*- module Dao module Validations class Validator - NotBlank = lambda{|value| !value.to_s.strip.empty?} unless defined?(NotBlank) + NotBlank = proc{|value| !value.to_s.strip.empty?} unless defined?(NotBlank) Cleared = 'Cleared'.freeze unless defined?(Cleared) + include Common + + def validator + self + end + + def validator=(validator) + raise NotImplementedError + end + class << Validator - def for(*args, &block) - new(*args, &block) + def mixin(*args, &block) + new(*args, &block).tap{|validator| validator.mixin = true} end end attr_accessor :object + attr_accessor :options attr_accessor :validations attr_accessor :errors attr_accessor :status - def initialize(object) - @object = object - @validations = Map.new - @errors = Errors.new - @status = Status.new + fattr(:attributes){ extract_attributes! } + alias_method(:data, :attributes) + + fattr(:mixin){ false } + + def initialize(*args, &block) + @object = args.shift + @options = Map.options_for(args) + + if args.size == 1 and @object and @object.is_a?(Hash) + object_keys = @object.keys.map{|key| key.to_s} + option_keys = %w( object validations errors status ) + + object_is_options = !object_keys.empty? && (object_keys - option_keys).empty? + + if object_is_options + @options = Map.for(@object) + @object = nil + end + end + + @object ||= (@options[:object] || Map.new) + @validations ||= (@options[:validations] || Map.new) + @errors ||= (@options[:errors] || Errors.new) + @status ||= (@options[:status] || Status.new) + + unless @object.respond_to?(:validator) + @object.send(:extend, Dao::Validations) + @object.validator = self + end + + #@object.extend(InstanceExec) unless @object.respond_to?(:instance_exec) end - fattr(:attributes) do + def extract_attributes!(object = @object) attributes = catch(:attributes) do - if @object.respond_to?(:attributes) - throw :attributes, @object.attributes + if object.respond_to?(:attributes) + throw :attributes, object.attributes end - if @object.instance_variable_defined?('@attributes') - throw :attributes, @object.instance_variable_get('@attributes') + if object.instance_variable_defined?('@attributes') + throw :attributes, object.instance_variable_get('@attributes') end - if @object.is_a?(Map) - throw :attributes, @object + if object.is_a?(Map) + throw :attributes, object end - if @object.respond_to?(:to_map) - throw :attributes, Map.new(@object.to_map) + if object.respond_to?(:to_map) + throw :attributes, Map.new(object.to_map) end - if @object.is_a?(Hash) - throw :attributes, Map.new(@object) + if object.is_a?(Hash) + throw :attributes, Map.new(object) end - if @object.respond_to?(:to_hash) - throw :attributes, Map.new(@object.to_hash) + if object.respond_to?(:to_hash) + throw :attributes, Map.new(object.to_hash) end - raise ArgumentError.new("found no attributes on #{ @object.inspect }(#{ @object.class.name })") + raise ArgumentError.new("found no attributes on #{ object.inspect }(#{ object.class.name })") end - case attributes - when Map - attributes - when Hash - Map.new(attributes) - else - raise(ArgumentError.new("#{ attributes.inspect } (#{ attributes.class })")) - end + @attributes = + case attributes + when Map + attributes + when Hash + Map.new(attributes) + else + raise(ArgumentError.new("#{ attributes.inspect } (#{ attributes.class })")) + end + + @attributes end - def add(*args, &block) - options = Map.options_for!(args) + def validates(*args, &block) block = args.pop if args.last.respond_to?(:call) block ||= NotBlank callback = Callback.new(options, &block) - validations.set(args => Callback::Chain.new) unless validations.has?(args) - validations.get(args).add(callback) + options = Map.options_for!(args) + key = key_for(args) + validations = stack.validations.last || self.validations + validations[key] ||= Callback::Chain.new + validations[key].add(callback) callback end - alias_method('validates', 'add') + alias_method('add', 'validates') - def run_validations!(*args) - run_validations(*args) - ensure - validated! + def validates_each(*args, &block) + options = Map.options_for!(args) + key = key_for(args) + + args.push(options) + + validates(*args) do |list| + Array(list).each_with_index do |item, index| + args = Dao.args_for_arity([item], block.arity) + validates(index, &block) + end + true + end end + def stack + @stack ||= Map[:validations, [], :prefixes, []] + end + + def prefixing(*prefix, &block) + prefix = Array(prefix).flatten.compact + push_prefix(prefix) + begin + block.call(*[prefix].slice(0, block.arity)) + ensure + pop_prefix + end + end + alias_method('validating', 'prefixing') + + def push_prefix(prefix) + prefix = Array(prefix).flatten.compact + stack.prefixes.push(prefix) + end + + def pop_prefix + stack.prefixes.pop + end + + def prefix + stack.prefixes.flatten.compact + end + + def key_for(*key) + prefix + Array(key).flatten.compact + end + + def get(key) + attributes.get(key_for(key)) + end + + def set(key, val) + attributes.set(key_for(key), val) + end + + def has(key) + attributes.has(key_for(key)) + end + + alias_method 'has?', 'has' + def validations_search_path @validations_search_path ||= ( - list = [ - object, - object.class.ancestors.map{|ancestor| ancestor.respond_to?(:validator) ? ancestor : nil} - ] - list.flatten! - list.compact! - list.reverse! - list + if mixin? + list = [ + object.respond_to?(:validator) ? object : nil, + object.class.ancestors.map{|ancestor| ancestor.respond_to?(:validator) ? ancestor : nil} + ] + list.flatten! + list.compact! + list.reverse! + list.uniq! + list + else + [self] + end ) end def validations_list - validations_search_path.map{|object| object.validator.validations} + validations_search_path.map{|object| object.validator.validations}.uniq end - def run_validations(*args) - object = args.first || @object + def run_validations(list = validations_list) + loop do + stack.validations.push(Map.new) - attributes.extend(InstanceExec) unless attributes.respond_to?(:instance_exec) + _run_validations(errors, list) - previous_errors = [] - new_errors = [] + added = stack.validations.pop + break if added.empty? + list = [added] + end - errors.each_message do |keys, message| - previous_errors.push([keys, message]) + if status.ok? and !errors.empty? + status.source = errors + status.update(412) end - errors.clear! - status.ok! - list = validations_list + if status == 412 and status.source == errors and errors.empty? + status.update(200) + end - list.each do |validations| - validations.depth_first_each do |keys, chain| + errors + ensure + validated!(true) + end + + alias_method 'run_validations!', 'run_validations' + alias_method 'validate', 'run_validations' + + def _run_validations(errors, list) + Array(list).each do |validations| + validations.each do |keys, chain| chain.each do |callback| next unless callback and callback.respond_to?(:to_proc) number_of_errors = errors.size value = attributes.get(keys) returned = catch(:validation) do - args = [value, attributes].slice(0, callback.arity) - attributes.instance_exec(*args, &callback) + args = Dao.args_for_arity([value, attributes], callback.arity) + + prefixing(keys) do + object.instance_exec(*args, &callback) + end end + errors_added = errors.size > number_of_errors + case returned when Hash - map = Map(returned) + map = Map.for(returned) valid = map[:valid] message = map[:message] - when TrueClass, FalseClass valid = returned message = nil - else - any_errors_added = errors.size > number_of_errors - valid = !any_errors_added + valid = !errors_added message = nil end + valid = false if errors_added + message ||= callback.options[:message] message ||= (value.to_s.strip.empty? ? 'is blank' : 'is invalid') - unless valid - new_errors.push([keys, message]) + if not valid + errors.add_from_source(keys, callback, message) else - new_errors.push([keys, Cleared]) + errors.delete_from_source(keys, callback) end end end end - - previous_errors.each do |keys, message| - errors.add(keys, message) unless new_errors.assoc(keys) - end - - new_errors.each do |keys, value| - next if value == Cleared - message = value - errors.add(keys, message) - end - - if status.ok? and !errors.empty? - status.update(412) - end - - errors end def validated? @validated = false unless defined?(@validated) @validated @@ -172,14 +277,10 @@ def validated!(boolean = true) @validated = !!boolean end - def validate - run_validations - end - def validate! raise Error.new("#{ object.class.name } is invalid!") unless valid? object end @@ -198,10 +299,10 @@ def valid?(*args) if forcing_validity? true else options = Map.options_for!(args) - validate #if(options[:validate] or !validated?) + run_validations errors.empty? and status.ok? end end def reset