diff --git a/Gemfile b/Gemfile index 848e4f11..68feb877 100644 --- a/Gemfile +++ b/Gemfile @@ -6,7 +6,7 @@ SOURCE = ENV.fetch('SOURCE', :git).to_sym REPO_POSTFIX = SOURCE == :path ? '' : '.git' DATAMAPPER = SOURCE == :path ? Pathname(__FILE__).dirname.parent : 'http://github.com/datamapper' DM_VERSION = '~> 1.1.0' -DO_VERSION = '~> 0.10.2' +DO_VERSION = '~> 0.10.4' DM_DO_ADAPTERS = %w[ sqlite postgres mysql oracle sqlserver ] gem 'dm-core', DM_VERSION, SOURCE => "#{DATAMAPPER}/dm-core#{REPO_POSTFIX}" diff --git a/lib/dm-validations.rb b/lib/dm-validations.rb index 7a6bb687..69b3717b 100644 --- a/lib/dm-validations.rb +++ b/lib/dm-validations.rb @@ -17,16 +17,22 @@ def try_call(*args) end module DataMapper - class Property - def self.new(model, name, options = {}) - property = super - property.model.auto_generate_validations(property) + module Validations + module PropertyExtensions + # @api private + def new(*) + property = super - # FIXME: explicit return needed for YARD to parse this properly - return property - end - end -end + property.model.auto_generate_validations(property) + + # FIXME: explicit return needed for YARD to parse this properly + return property + end + end # module PropertyExtensions + end # module Validations + + Property.extend Validations::PropertyExtensions +end # module DataMapper require 'dm-validations/exceptions' require 'dm-validations/validation_errors' @@ -55,8 +61,6 @@ module Validations Model.append_inclusions self - extend Chainable - def self.included(model) model.extend ClassMethods end @@ -64,23 +68,19 @@ def self.included(model) # Ensures the object is valid for the context provided, and otherwise # throws :halt and returns false. # - chainable do - def save(context = default_validation_context) - validation_context(context) { super() } - end + def save(context = default_validation_context) + model.validators.assert_valid(context) + Validations::Context.in_context(context) { super() } end - chainable do - def update(attributes = {}, context = default_validation_context) - validation_context(context) { super(attributes) } - end + def update(attributes = {}, context = default_validation_context) + model.validators.assert_valid(context) + Validations::Context.in_context(context) { super(attributes) } end - chainable do - def save_self(*) - return false unless !dirty_self? || validation_context_stack.empty? || valid?(current_validation_context) - super - end + def save_self(*) + return false unless !dirty_self? || Validations::Context.stack.empty? || valid?(model.validators.current_context) + super end # Return the ValidationErrors @@ -91,7 +91,7 @@ def errors # Mark this resource as validatable. When we validate associations of a # resource we can check if they respond to validatable? before trying to - # recursivly validate them + # recursively validate them # def validatable? true @@ -99,31 +99,24 @@ def validatable? # Alias for valid?(:default) # + # TODO: deprecate def valid_for_default? valid?(:default) end # Check if a resource is valid in a given context # + # @api public def valid?(context = :default) - klass = respond_to?(:model) ? model : self.class - klass.validators.execute(context, self) + model = respond_to?(:model) ? self.model : self.class + model.validators.execute(context, self) end + # @api semipublic def validation_property_value(name) __send__(name) if respond_to?(name, true) 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. - def validation_property(field_name) - if respond_to?(:model) && (properties = model.properties(repository.name)) && properties.named?(field_name) - properties[field_name] - end - end - module ClassMethods include DataMapper::Validations::ValidatesPresence include DataMapper::Validations::ValidatesAbsence @@ -142,13 +135,16 @@ module ClassMethods # Return the set of contextual validators or create a new one # def validators - @validators ||= ContextualValidators.new + @validators ||= ContextualValidators.new(self) end def inherited(base) super - validators.contexts.each do |context, validators| - base.validators.context(context).concat(validators) + self.validators.contexts.each do |context, validators| + validators.each do |v| + options = v.options.merge(:context => context) + base.validators.add(v.class, v.field_name, options) + end end end @@ -160,57 +156,31 @@ def create(attributes = {}, *args) private - # Clean up the argument list and return a opts hash, including the - # merging of any default opts. Set the context to default if none is - # provided. Also allow :context to be aliased to :on, :when & group - # - def opts_from_validator_args(args, defaults = nil) - opts = args.last.kind_of?(Hash) ? args.pop.dup : {} - context = opts.delete(:group) || opts.delete(:on) || opts.delete(:when) || opts.delete(:context) || :default - opts[:context] = Array(context) - opts.update(defaults) unless defaults.nil? - opts - end - # Given a new context create an instance method of # valid_for_? which simply calls valid?(context) # if it does not already exist # - def create_context_instance_methods(context) - name = "valid_for_#{context.to_s}?" - unless respond_to?(:resource_method_defined) ? resource_method_defined?(name) : instance_methods.include?(name) - class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{name} # def valid_for_signup? - valid?(#{context.to_sym.inspect}) # valid?(:signup) - end # end + def self.create_context_instance_methods(model, context) + # TODO: deprecate `valid_for_#{context}?` + # what's wrong with requiring the caller to pass the context as an arg? + # eg, `valid?(:context)` + # these methods are handy for symbol-based callbacks, + # eg. `:if => :valid_for_context?` + # but these methods are so trivial to add where needed, making it + # overkill to do this for all contexts on all validated objects. + context = context.to_sym + + name = "valid_for_#{context}?" + present = model.respond_to?(:resource_method_defined) ? model.resource_method_defined?(name) : model.instance_methods.include?(name) + unless present + model.class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{name} # def valid_for_signup? + valid?(#{context.inspect}) # valid?(:signup) + end # end RUBY end end - # Create a new validator of the given klazz and push it onto the - # requested context for each of the attributes in the fields list - # @param [Hash] opts - # Options supplied to validation macro, example: - # {:context=>:default, :maximum=>50, :allow_nil=>true, :message=>nil} - # - # @param [Array] fields - # Fields given to validation macro, example: - # [:first_name, :last_name] in validates_presence_of :first_name, :last_name - # - # @param [Class] klazz - # Validator class, example: DataMapper::Validations::LengthValidator - def add_validator_to_context(opts, fields, validator_class) - fields.each do |field| - validator = validator_class.new(field, opts.dup) - - opts[:context].each do |context| - validator_contexts = validators.context(context) - next if validator_contexts.include?(validator) - validator_contexts << validator - create_context_instance_methods(context) - end - end - end end # module ClassMethods end # module Validations diff --git a/lib/dm-validations/auto_validate.rb b/lib/dm-validations/auto_validate.rb index c56591d3..491a19c5 100644 --- a/lib/dm-validations/auto_validate.rb +++ b/lib/dm-validations/auto_validate.rb @@ -1,14 +1,14 @@ module DataMapper - class Property - # for options_with_message - accept_options :message, :messages, :set, :validates, :auto_validation, :format - end + # for options_with_message + Property.accept_options :message, :messages, :set, :validates, :auto_validation, :format module Validations module AutoValidations @disable_auto_validations = false # adds message for validator + # + # @api private def options_with_message(base_options, property, validator_name) options = base_options.clone opts = property.options @@ -26,6 +26,8 @@ def options_with_message(base_options, property, validator_name) # disables generation of validations for # duration of given block + # + # @api public def without_auto_validations @disable_auto_validations = true yield @@ -81,6 +83,7 @@ def without_auto_validations # :message => "Some message" # It is just shortcut if only one validation option is set # + # @api private def auto_generate_validations(property) return if (disabled_auto_validations? || skip_auto_validation_for?(property)) @@ -108,6 +111,7 @@ def auto_generate_validations(property) # @return [TrueClass, FalseClass] # true if auto validation is currently disabled # + # @api semipublic def disabled_auto_validations? @disable_auto_validations || false end @@ -122,11 +126,13 @@ def disabled_auto_validations? # true for properties with :auto_validation option that has # positive value # + # @api private def skip_auto_validation_for?(property) (property.options.key?(:auto_validation) && !property.options[:auto_validation]) end + # @api private def infer_presence_validation_for(property, options) return if skip_presence_validation?(property) @@ -137,6 +143,7 @@ def infer_presence_validation_for(property, options) ) end + # @api private def infer_length_validation_for(property, options) return unless (property.kind_of?(DataMapper::Property::String) || property.kind_of?(DataMapper::Property::Text)) @@ -158,6 +165,7 @@ def infer_length_validation_for(property, options) ) end + # @api private def infer_format_validation_for(property, options) return unless property.options.key?(:format) @@ -170,6 +178,7 @@ def infer_format_validation_for(property, options) ) end + # @api private def infer_uniqueness_validation_for(property, options) return unless property.options.key?(:unique) @@ -191,6 +200,7 @@ def infer_uniqueness_validation_for(property, options) end end + # @api private def infer_within_validation_for(property, options) return unless property.options.key?(:set) @@ -203,6 +213,7 @@ def infer_within_validation_for(property, options) ) end + # @api private def infer_type_validation_for(property, options) return if property.respond_to?(:custom?) && property.custom? @@ -242,6 +253,7 @@ def infer_type_validation_for(property, options) private + # @api private def skip_presence_validation?(property) property.allow_blank? || property.serial? end diff --git a/lib/dm-validations/contextual_validators.rb b/lib/dm-validations/contextual_validators.rb index bcf7eb94..de8e928c 100644 --- a/lib/dm-validations/contextual_validators.rb +++ b/lib/dm-validations/contextual_validators.rb @@ -13,12 +13,15 @@ class ContextualValidators # def_delegators :@contexts, :empty?, :each + def_delegators :@attributes, :[] include Enumerable - attr_reader :contexts + attr_reader :contexts, :attributes - def initialize - @contexts = {} + def initialize(model = nil) + @model = model + @contexts = {} + @attributes = {} end # @@ -28,81 +31,228 @@ def initialize # Return an array of validators for a named context # # @param [String] - # Context name for which return validators + # Context name for which to return validators # @return [Array] - # An array of validators + # An array of validators bound to the given context def context(name) - contexts[name] ||= [] + contexts[name] ||= OrderedSet.new + end + + # Return an array of validators for a named property + # + # @param [Symbol] + # Property name for which to return validators + # @return [Array] + # An array of validators bound to the given property + def attribute(name) + attributes[name] ||= OrderedSet.new end # Clear all named context validators off of the resource # def clear! contexts.clear + attributes.clear + end + + # Create a new validator of the given klazz and push it onto the + # requested context for each of the attributes in +attributes+ + # + # @param [DataMapper::Validations::GenericValidator] validator_class + # Validator class, example: DataMapper::Validations::LengthValidator + # + # @param [Array] attributes + # Attribute names given to validation macro, example: + # [:first_name, :last_name] in validates_presence_of :first_name, :last_name + # + # @param [Hash] options + # Options supplied to validation macro, example: + # {:context=>:default, :maximum=>50, :allow_nil=>true, :message=>nil} + # + # @option [Symbol] :context + # the context in which the new validator should be run + # @option [Boolean] :allow_nil + # whether or not the new validator should allow nil values + # @option [Boolean] :message + # the error message the new validator will provide on validation failure + def add(validator_class, *attributes) + options = attributes.last.kind_of?(Hash) ? attributes.pop.dup : {} + normalize_options(options) + + attributes.each do |attribute| + # TODO: is :context part of the Validator state (ie, intrinsic), + # or is it just membership in a collection? + validator_options = DataMapper::Ext::Hash.except(options, :context) + validator = validator_class.new(attribute, validator_options) + attribute_validators = self.attribute(attribute) + attribute_validators << validator unless attribute_validators.include?(validator) + + options[:context].each do |context| + context_validators = self.context(context) + next if context_validators.include?(validator) + context_validators << validator + # TODO: eliminate this, then eliminate the @model ivar entirely + Validations::ClassMethods.create_context_instance_methods(@model, context) if @model + end + end + end + + # Clean up the argument list and return a opts hash, including the + # merging of any default opts. Set the context to default if none is + # provided. Also allow :context to be aliased to :on, :when & :group + # + # @param [Hash] options + # the options to be normalized + # @param [NilClass, Hash] defaults + # default keys/values to set on normalized options + # + # @return [Hash] + # the normalized options + # + # @api private + def normalize_options(options, defaults = nil) + context = [ + options.delete(:group), + options.delete(:on), + options.delete(:when), + options.delete(:context) + ].compact.first || :default + + options[:context] = Array(context) + options.update(defaults) unless defaults.nil? + options + end + + # Returns the current validation context on the stack if valid for this model, + # nil if no contexts are defined for the model (and no contexts are on + # the validation stack), or :default if the current context is invalid for + # this model or no contexts have been defined for this model and + # no context is on the stack. + # + # @return [Symbol] + # the current validation context from the stack (if valid for this model), + # nil if no context is on the stack and no contexts are defined for this model, + # or :default if the context on the stack is invalid for this model or + # no context is on the stack and this model has at least one validation context + # + # @api private + # + # TODO: simplify the semantics of #current_context, #valid? + def current_context + context = Validations::Context.current + valid_context?(context) ? context : :default + end + + # Test if the context is valid for the model + # + # @param [Symbol] context + # the context to test + # + # @return [Boolean] + # true if the context is valid for the model + # + # @api private + # + # TODO: investigate removing the `contexts.empty?` test here. + def valid_context?(context) + contexts.empty? || contexts.include?(context) + end + + # Assert that the given context is valid for this model + # + # @param [Symbol] context + # the context to test + # + # @raise [InvalidContextError] + # raised if the context is not valid for this model + # + # @api private + # + # TODO: is this method actually needed? + def assert_valid(context) + unless valid_context?(context) + raise InvalidContextError, "#{context} is an invalid context, known contexts are #{contexts.keys.inspect}" + end end # Execute all validators in the named context against the target. # Load together any properties that are designated lazy but are not # yet loaded. Optionally only validate dirty properties. # - # @param [Symbol] - # named_context the context we are validating against - # @param [Object] - # target the resource that we are validating + # @param [Symbol] named_context + # the context we are validating against + # @param [Object] target + # the resource that we are validating # @return [Boolean] # true if all are valid, otherwise false def execute(named_context, target) target.errors.clear! - runnable_validators = context(named_context).select{ |validator| validator.execute?(target) } - validators = runnable_validators.dup + available_validators = context(named_context) + executable_validators = available_validators.select { |v| v.execute?(target) } - # By default we start the list with the full set of runnable + # By default we start the list with the full set of executable # validators. # # In the case of a new Resource or regular ruby class instance, # everything needs to be validated completely, and no eager-loading # logic should apply. # - # In the case of a DM::Resource that isn't new, we optimize: - # - # 1. Eager-load all lazy, not-yet-loaded properties that need - # validation, all at once. - # - # 2. Limit run validators to - # - those applied to dirty attributes only, - # - those that should always run (presence/absence) - # - those that don't reference any real properties (field-less - # block validators, validations in virtual attributes) - if target.kind_of?(DataMapper::Resource) && !target.new? - attrs = target.attributes.keys - dirty_attrs = target.dirty_attributes.keys.map{ |p| p.name } - validators = runnable_validators.select{|v| - !attrs.include?(v.field_name) || dirty_attrs.include?(v.field_name) - } - - # Load all lazy, not-yet-loaded properties that need validation, - # all at once. - fields_to_load = validators.map{|v| - target.class.properties[v.field_name] - }.compact.select {|p| - p.lazy? && !p.loaded?(target) - } - - target.__send__(:eager_load, fields_to_load) - - # Finally include any validators that should always run or don't - # reference any real properties (field-less block vaildators). - validators |= runnable_validators.select do |v| - v.kind_of?(MethodValidator) || - v.kind_of?(PresenceValidator) || - v.kind_of?(AbsenceValidator) + # @see #validators_for_resource + validators = + if target.kind_of?(DataMapper::Resource) && !target.new? + validators_for_resource(target, executable_validators) + else + executable_validators end - end validators.map { |validator| validator.call(target) }.all? end + # In the case of a DM::Resource that isn't new, we optimize: + # + # 1. Eager-load all lazy, not-yet-loaded properties that need + # validation, all at once. + # + # 2. Limit run validators to + # - those applied to dirty attributes only, + # - those that should always run (presence/absence) + # - those that don't reference any real properties (attribute-less + # block validators, validations in virtual attributes) + def validators_for_resource(resource, all_validators) + attrs = resource.attributes + dirty_attrs = Hash[resource.dirty_attributes.map { |p, value| [p.name, value] }] + validators = all_validators.select { |v| + !attrs.include?(v.field_name) || dirty_attrs.include?(v.field_name) + } + + load_validated_properties(resource, validators) + + # Finally include any validators that should always run or don't + # reference any real properties (field-less block vaildators). + validators |= all_validators.select do |v| + v.kind_of?(MethodValidator) || + v.kind_of?(PresenceValidator) || + v.kind_of?(AbsenceValidator) + end + + validators + end + + # Load all lazy, not-yet-loaded properties that need validation, + # all at once. + def load_validated_properties(resource, validators) + properties = resource.model.properties + + properties_to_load = validators.map { |validator| + properties[validator.field_name] + }.compact.select { |property| + property.lazy? && !property.loaded?(resource) + } + + resource.__send__(:eager_load, properties_to_load) + end + end # module ContextualValidators end # module Validations end # module DataMapper diff --git a/lib/dm-validations/support/context.rb b/lib/dm-validations/support/context.rb index cbe53e4b..a7bbf636 100644 --- a/lib/dm-validations/support/context.rb +++ b/lib/dm-validations/support/context.rb @@ -2,93 +2,56 @@ module DataMapper module Validations # Module with validation context functionality. # - # Contexts are implemented using a simple array based - # stack that is thread local. The default context can be - # altered by overwriting default_validation_context or - # will default to :default + # Contexts are implemented using a thread-local array-based stack. # module Context - # TODO: document - # @api private - def default_validation_context - current_validation_context || :default + # Execute a block of code within a specific validation context + # + # @param [Symbol] context + # the context to execute the block of code within + # + # @api semipublic + def self.in_context(context) + stack << context + yield + ensure + stack.pop end - protected - - # Pushes given context on top of context stack and yields - # given block, then pops the stack. During block execution - # contexts previously pushed onto the stack have no effect. - # - # @api private - def validation_context(context = default_validation_context) - assert_valid_context(context) - validation_context_stack << context - begin - yield - ensure - validation_context_stack.pop - end + # Get the current validation context or nil (if no context is on the stack). + # + # @return [Symbol, NilClass] + # The current validation context (for the current thread), + # or nil if no current context is on the stack + def self.current + stack.last end - private - - # Initializes (if necessary) and returns current scope stack - # @api private - def validation_context_stack + # The (thread-local) validation context stack + # This allows object graphs to be saved within potentially nested contexts + # without having to pass the validation context throughout + # + # @api semipublic + def self.stack Thread.current[:dm_validations_context_stack] ||= [] end - # Returns the current validation context or nil if none has been - # pushed. - # - # @api private - def current_validation_context - context = validation_context_stack.last - valid_context?(context) ? context : :default - end - - # Return the contexts for the model - # - # @return [Hash] - # the hash of contexts for the model - # - # @api private - def contexts - model.validators.contexts - end - - # Test if the context is valid for the model - # - # @param [Symbol] context - # the context to test - # - # @return [Boolean] - # true if the context is valid for the model - # - # @api private - def valid_context?(context) - contexts.empty? || contexts.key?(context) - end - - # Assert that the context is valid for this model - # - # @param [Symbol] context - # the context to test - # - # @raise [InvalidContextError] - # raised if the context is not valid for this model - # - # @api private - def assert_valid_context(context) - unless valid_context?(context) - raise InvalidContextError, "#{context} is an invalid context, known contexts are #{contexts.keys.inspect}" - end + # The default validation context for this Resource. + # This Resource's default context can be overridden by implementing + # #default_validation_context + # + # @return [Symbol] + # the current validation context from the context stack + # (if valid for this model), or :default + # + # @api semipublic + def default_validation_context + model.validators.current_context || :default end - end + end # module Context include Context - end -end + end # module Validations +end # module DataMapper diff --git a/lib/dm-validations/validators/absent_field_validator.rb b/lib/dm-validations/validators/absent_field_validator.rb index a080c24c..150e9ef5 100644 --- a/lib/dm-validations/validators/absent_field_validator.rb +++ b/lib/dm-validations/validators/absent_field_validator.rb @@ -49,11 +49,7 @@ module ValidatesAbsence # end # def validates_absence_of(*fields) - add_validator_to_context( - opts_from_validator_args(fields), - fields, - DataMapper::Validations::AbsenceValidator - ) + validators.add(AbsenceValidator, *fields) end deprecate :validates_absent, :validates_absence_of diff --git a/lib/dm-validations/validators/acceptance_validator.rb b/lib/dm-validations/validators/acceptance_validator.rb index 6d87cbcb..e726184b 100644 --- a/lib/dm-validations/validators/acceptance_validator.rb +++ b/lib/dm-validations/validators/acceptance_validator.rb @@ -69,11 +69,7 @@ module ValidatesAcceptance # # terms_accepted is one of ["1", 1, "true", true, "t"] # def validates_acceptance_of(*fields) - add_validator_to_context( - opts_from_validator_args(fields), - fields, - DataMapper::Validations::AcceptanceValidator - ) + validators.add(AcceptanceValidator, *fields) end deprecate :validates_is_accepted, :validates_acceptance_of diff --git a/lib/dm-validations/validators/block_validator.rb b/lib/dm-validations/validators/block_validator.rb index 22550e22..eab8bd43 100644 --- a/lib/dm-validations/validators/block_validator.rb +++ b/lib/dm-validations/validators/block_validator.rb @@ -44,20 +44,17 @@ def validates_with_block(*fields, &block) # create method and pass it to MethodValidator unless block_given? - raise ArgumentError.new('You need to pass a block to validates_with_block method') + raise ArgumentError, 'You need to pass a block to validates_with_block method' end method_name = "__validates_with_block_#{@__validates_with_block_count}".to_sym - define_method(method_name, block) + define_method(method_name, &block) - opts = opts_from_validator_args(fields) - opts[:method] = method_name + options = fields.last.is_a?(Hash) ? fields.last.pop.dup : {} + options[:method] = method_name + fields = [method_name] if fields.empty? - add_validator_to_context( - opts, - fields.empty? ? [method_name] : fields, - DataMapper::Validations::MethodValidator - ) + validators.add(MethodValidator, *fields + [options]) end end # module ValidatesWithMethod end # module Validations diff --git a/lib/dm-validations/validators/confirmation_validator.rb b/lib/dm-validations/validators/confirmation_validator.rb index ae58c76c..30b9fc1c 100644 --- a/lib/dm-validations/validators/confirmation_validator.rb +++ b/lib/dm-validations/validators/confirmation_validator.rb @@ -82,11 +82,7 @@ module ValidatesConfirmation # # email == email_repeated # def validates_confirmation_of(*fields) - add_validator_to_context( - opts_from_validator_args(fields), - fields, - DataMapper::Validations::ConfirmationValidator - ) + validators.add(ConfirmationValidator, *fields) end deprecate :validates_is_confirmed, :validates_confirmation_of diff --git a/lib/dm-validations/validators/format_validator.rb b/lib/dm-validations/validators/format_validator.rb index e957f45f..c7d601ad 100644 --- a/lib/dm-validations/validators/format_validator.rb +++ b/lib/dm-validations/validators/format_validator.rb @@ -115,11 +115,7 @@ module ValidatesFormat # # zip_code is a string of 5 digits # def validates_format_of(*fields) - add_validator_to_context( - opts_from_validator_args(fields), - fields, - DataMapper::Validations::FormatValidator - ) + validators.add(FormatValidator, *fields) end deprecate :validates_format, :validates_format_of diff --git a/lib/dm-validations/validators/generic_validator.rb b/lib/dm-validations/validators/generic_validator.rb index 81131622..b8f3fbd9 100644 --- a/lib/dm-validations/validators/generic_validator.rb +++ b/lib/dm-validations/validators/generic_validator.rb @@ -84,34 +84,31 @@ def call(target) # @return [Boolean] # true if should be run, otherwise false. # + # @api private def execute?(target) if unless_clause = self.unless_clause - if unless_clause.kind_of?(Symbol) - return !target.__send__(unless_clause) - end - - if unless_clause.respond_to?(:call) - return !unless_clause.call(target) - end + !evaluate_conditional_clause(target, unless_clause) + elsif if_clause = self.if_clause + evaluate_conditional_clause(target, if_clause) + else + true end + end - if if_clause = self.if_clause - if if_clause.kind_of?(Symbol) - return target.__send__(if_clause) - end - - if if_clause.respond_to?(:call) - return if_clause.call(target) - end + # @api private + def evaluate_conditional_clause(target, clause) + if clause.kind_of?(Symbol) + target.__send__(clause) + elsif clause.respond_to?(:call) + clause.call(target) end - - true end # Set the default value for allow_nil and allow_blank # # @param [Boolean] default value # + # @api private def set_optional_by_default(default = true) [ :allow_nil, :allow_blank ].each do |key| @options[key] = true unless options.key?(key) @@ -128,6 +125,7 @@ def set_optional_by_default(default = true) # @return [Boolean] # true if blank/nil is allowed, and the value is blank/nil. # + # @api private def optional?(value) if value.nil? @options[:allow_nil] || @@ -157,7 +155,7 @@ def ==(other) self.field_name == other.field_name && self.if_clause == other.if_clause && self.unless_clause == other.unless_clause && - self.instance_variable_get(:@options) == other.instance_variable_get(:@options) + self.options == other.options end def inspect @@ -166,6 +164,21 @@ def inspect alias_method :to_s, :inspect + private + + # 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 GenericValidator end # module Validations end # module DataMapper diff --git a/lib/dm-validations/validators/length_validator.rb b/lib/dm-validations/validators/length_validator.rb index 3bd207da..2441c759 100644 --- a/lib/dm-validations/validators/length_validator.rb +++ b/lib/dm-validations/validators/length_validator.rb @@ -240,11 +240,7 @@ module ValidatesLength # # just_right is between 1 and 10 (inclusive of both 1 and 10) # def validates_length_of(*fields) - add_validator_to_context( - opts_from_validator_args(fields), - fields, - DataMapper::Validations::LengthValidator - ) + validators.add(LengthValidator, *fields) end deprecate :validates_length, :validates_length_of diff --git a/lib/dm-validations/validators/method_validator.rb b/lib/dm-validations/validators/method_validator.rb index fe4f72c8..fb3a9a56 100644 --- a/lib/dm-validations/validators/method_validator.rb +++ b/lib/dm-validations/validators/method_validator.rb @@ -57,11 +57,7 @@ module ValidatesWithMethod # # wrong zip code" unless zip_code == "94301" # end def validates_with_method(*fields) - add_validator_to_context( - opts_from_validator_args(fields), - fields, - DataMapper::Validations::MethodValidator - ) + validators.add(MethodValidator, *fields) end end # module ValidatesWithMethod end # module Validations diff --git a/lib/dm-validations/validators/numeric_validator.rb b/lib/dm-validations/validators/numeric_validator.rb index e7786808..0a2e095a 100644 --- a/lib/dm-validations/validators/numeric_validator.rb +++ b/lib/dm-validations/validators/numeric_validator.rb @@ -173,11 +173,7 @@ module ValidatesNumericality # Use to restrict allowed values to integers. # def validates_numericality_of(*fields) - add_validator_to_context( - opts_from_validator_args(fields), - fields, - DataMapper::Validations::NumericalityValidator - ) + validators.add(NumericalityValidator, *fields) end deprecate :validates_is_number, :validates_numericality_of diff --git a/lib/dm-validations/validators/primitive_validator.rb b/lib/dm-validations/validators/primitive_validator.rb index 68ee585c..2ceeac4f 100644 --- a/lib/dm-validations/validators/primitive_validator.rb +++ b/lib/dm-validations/validators/primitive_validator.rb @@ -6,7 +6,7 @@ class PrimitiveTypeValidator < GenericValidator def call(target) value = target.validation_property_value(field_name) - property = target.validation_property(field_name) + property = get_resource_property(target, field_name) return true if value.nil? || property.primitive?(value) @@ -49,11 +49,7 @@ module ValidatesPrimitiveType # # casted into a Date object. # end def validates_primitive_type_of(*fields) - add_validator_to_context( - opts_from_validator_args(fields), - fields, - DataMapper::Validations::PrimitiveTypeValidator - ) + validators.add(PrimitiveTypeValidator, *fields) end deprecate :validates_is_primitive, :validates_primitive_type_of diff --git a/lib/dm-validations/validators/required_field_validator.rb b/lib/dm-validations/validators/required_field_validator.rb index 93b43c06..9999ffaa 100644 --- a/lib/dm-validations/validators/required_field_validator.rb +++ b/lib/dm-validations/validators/required_field_validator.rb @@ -5,8 +5,8 @@ module Validations class PresenceValidator < GenericValidator def call(target) - value = target.validation_property_value(field_name) - property = target.validation_property(field_name) + value = target.validation_property_value(field_name) + property = get_resource_property(target, field_name) return true if present?(value, property) error_message = @options[:message] || default_error(property) @@ -74,11 +74,7 @@ module ValidatesPresence # # all three attributes are !blank? # end def validates_presence_of(*fields) - add_validator_to_context( - opts_from_validator_args(fields), - fields, - DataMapper::Validations::PresenceValidator - ) + validators.add(PresenceValidator, *fields) end deprecate :validates_present, :validates_presence_of diff --git a/lib/dm-validations/validators/uniqueness_validator.rb b/lib/dm-validations/validators/uniqueness_validator.rb index 15d4b1d4..67979ae6 100644 --- a/lib/dm-validations/validators/uniqueness_validator.rb +++ b/lib/dm-validations/validators/uniqueness_validator.rb @@ -58,11 +58,7 @@ module ValidatesUniqueness # Validate the uniqueness of a field # def validates_uniqueness_of(*fields) - add_validator_to_context( - opts_from_validator_args(fields), - fields, - DataMapper::Validations::UniquenessValidator - ) + validators.add(UniquenessValidator, *fields) end deprecate :validates_is_unique, :validates_uniqueness_of diff --git a/lib/dm-validations/validators/within_validator.rb b/lib/dm-validations/validators/within_validator.rb index 85d1485e..5905a5a2 100644 --- a/lib/dm-validations/validators/within_validator.rb +++ b/lib/dm-validations/validators/within_validator.rb @@ -15,6 +15,7 @@ def call(target) return true if optional?(value) return true if @options[:set].include?(value) + n = 1.0/0 set = @options[:set] msg = @options[:message] @@ -35,9 +36,6 @@ def call(target) false end - def n - 1.0/0 - end end # class WithinValidator @@ -45,11 +43,7 @@ module ValidatesWithin # Validate that value of a field if within a range/set # def validates_within(*fields) - add_validator_to_context( - opts_from_validator_args(fields), - fields, - DataMapper::Validations::WithinValidator - ) + validators.add(WithinValidator, *fields) end end # module ValidatesWithin end # module Validations diff --git a/spec/integration/datamapper_models/association_validation_spec.rb b/spec/integration/datamapper_models/association_validation_spec.rb index eab6ddcd..97a536e2 100644 --- a/spec/integration/datamapper_models/association_validation_spec.rb +++ b/spec/integration/datamapper_models/association_validation_spec.rb @@ -5,10 +5,13 @@ DataMapper::Validations::Fixtures::ProductCompany.auto_migrate! DataMapper::Validations::Fixtures::Product.auto_migrate! - @parent = DataMapper::Validations::Fixtures::ProductCompany.create(:title => "Apple", :flagship_product => "Macintosh") + parent_model = DataMapper::Validations::Fixtures::ProductCompany + @parent = parent_model.new(:title => "Apple", :flagship_product => "Macintosh") @parent.should be_valid + @parent.save.should be_true - @model = DataMapper::Validations::Fixtures::Product.new(:name => "MacBook Pro", :company => @parent) + model_model = DataMapper::Validations::Fixtures::Product + @model = model_model.new(:name => "MacBook Pro", :company => @parent) @model.should be_valid end