Permalink
Browse files

More initial changes.

  • Loading branch information...
1 parent c59c151 commit 61bd7d91051fbb958478e2cb018bad677d397f0e Blake Gentry committed Sep 1, 2009
@@ -64,7 +64,7 @@ def assert_good_or_bad_if_key(key, value, message_key=:message) #:nodoc:
# Default allow_nil? validation. It accepts the message_key which is
# the key which contain the message in @options.
#
- # It also gets an allow_nil message on remarkable.active_record.allow_nil
+ # It also gets an allow_nil message on remarkable.data_mapper.allow_nil
# to be used as default.
#
def allow_nil?(message_key=:message) #:nodoc:
@@ -74,7 +74,7 @@ def allow_nil?(message_key=:message) #:nodoc:
# Default allow_blank? validation. It accepts the message_key which is
# the key which contain the message in @options.
#
- # It also gets an allow_blank message on remarkable.active_record.allow_blank
+ # It also gets an allow_blank message on remarkable.data_mapper.allow_blank
# to be used as default.
#
def allow_blank?(message_key=:message) #:nodoc:
@@ -93,7 +93,7 @@ def bad?(value, message_sym=:message) #:nodoc:
assert_bad_value(@subject, @attribute, value, @options[message_sym])
end
- # Asserts that an Active Record model validates with the passed
+ # Asserts that a DataMapper model validates with the passed
# <tt>value</tt> by making sure the <tt>error_message_to_avoid</tt> is not
# contained within the list of errors for that attribute.
#
@@ -119,7 +119,7 @@ def assert_good_value(model, attribute, value, error_message_to_avoid=//) # :nod
assert_does_not_contain(model.errors.on(attribute), error_message_to_avoid)
end
- # Asserts that an Active Record model invalidates the passed
+ # Asserts that a DataMapper model invalidates the passed
# <tt>value</tt> by making sure the <tt>error_message_to_expect</tt> is
# contained within the list of errors for that attribute.
#
@@ -173,7 +173,7 @@ def assert_bad_value(model, attribute, value, error_message_to_expect=:invalid)
# macros would start to pass when they shouldn't.
#
# Using the underlying mechanism inside DataMapper makes us free from
- # all thos errors.
+ # all those errors.
#
# We replace {{count}} interpolation for 12345 which later is replaced
# by a regexp which contains \d+.
@@ -0,0 +1,233 @@
+module Remarkable
+ module DataMapper
+ module Matchers
+ class ValidatesIsUniqueMatcher < Remarkable::DataMapper::Base #:nodoc:
+ arguments :collection => :attributes, :as => :attribute
+
+ optional :message
+ optional :scope, :splat => true
+ optional :case_sensitive, :allow_nil, :allow_blank, :default => true
+
+ collection_assertions :find_first_object?, :responds_to_scope?, :is_unique?, :case_sensitive?,
+ :valid_with_new_scope?, :allow_nil?, :allow_blank?
+
+ default_options :message => :taken
+
+ before_assert do
+ @options[:scope] = [*@options[:scope]].compact if @options[:scope]
+ end
+
+ private
+
+ # Tries to find an object in the database. If allow_nil and/or allow_blank
+ # is given, we must find a record which is not nil or not blank.
+ #
+ # We should also ensure that the object retrieved from the database
+ # is not the @subject.
+ #
+ # If any of these attempts fail, an error is raised.
+ #
+ def find_first_object?
+ conditions, message = if @options[:allow_nil]
+ [ ["#{@attribute} IS NOT NULL"], " with #{@attribute} not nil" ]
+ elsif @options[:allow_blank]
+ [ ["#{@attribute} != ''"], " with #{@attribute} not blank" ]
+ else
+ [ [], "" ]
+ end
+
+ unless @subject.new_record?
+ primary_key = subject_class.primary_key
+
+ message << " which is different from the subject record (the object being validated is the same as the one in the database)"
+ conditions << "#{subject_class.primary_key} != '#{@subject.send(primary_key)}'"
+ end
+
+ options = conditions.empty? ? {} : { :conditions => conditions.join(' AND ') }
+
+ return true if @existing = subject_class.find(:first, options)
+ raise ScriptError, "could not find a #{subject_class} record in the database" + message
+ end
+
+ # Set subject scope to be equal to the object found.
+ #
+ def responds_to_scope?
+ (@options[:scope] || []).each do |scope|
+ setter = :"#{scope}="
+
+ return false, :method => setter unless @subject.respond_to?(setter)
+ return false, :method => scope unless @existing.respond_to?(scope)
+
+ @subject.send(setter, @existing.send(scope))
+ end
+ true
+ end
+
+ # Check if the attribute given is valid and if the validation fails for equal values.
+ #
+ def is_unique?
+ @value = @existing.send(@attribute)
+ return bad?(@value)
+ end
+
+ # If :case_sensitive is given and it's false, we swap the case of the
+ # value used in :is_unique? and see if the test object remains valid.
+ #
+ # If :case_sensitive is given and it's true, we swap the case of the
+ # value used in is_unique? and see if the test object is not valid.
+ #
+ # This validation will only occur if the test object is a String.
+ #
+ def case_sensitive?
+ return true unless @value.is_a?(String)
+ assert_good_or_bad_if_key(:case_sensitive, @value.swapcase)
+ end
+
+ # Now test that the object is valid when changing the scoped attribute.
+ #
+ def valid_with_new_scope?
+ (@options[:scope] || []).each do |scope|
+ setter = :"#{scope}="
+
+ previous_scope_value = @subject.send(scope)
+ @subject.send(setter, new_value_for_scope(scope))
+ return false, :method => scope unless good?(@value)
+
+ @subject.send(setter, previous_scope_value)
+ end
+ true
+ end
+
+ # Change the existing object attribute to nil to run allow nil
+ # validations. If we find any problem while updating the @existing
+ # record, it's because we can't save nil values in the database. So it
+ # passes when :allow_nil is false, but should raise an error when
+ # :allow_nil is true
+ #
+ def allow_nil?
+ return true unless @options.key?(:allow_nil)
+
+ begin
+ @existing.update_attribute(@attribute, nil)
+ rescue ::ActiveRecord::StatementInvalid => e
+ raise ScriptError, "You declared that #{@attribute} accepts nil values in validate_uniqueness_of, " <<
+ "but I cannot save nil values in the database, got: #{e.message}" if @options[:allow_nil]
+ return true
+ end
+
+ super
+ end
+
+ # Change the existing object attribute to blank to run allow blank
+ # validation. It uses the same logic as :allow_nil.
+ #
+ def allow_blank?
+ return true unless @options.key?(:allow_blank)
+
+ begin
+ @existing.update_attribute(@attribute, '')
+ rescue ::ActiveRecord::StatementInvalid => e
+ raise ScriptError, "You declared that #{@attribute} accepts blank values in validate_uniqueness_of, " <<
+ "but I cannot save blank values in the database, got: #{e.message}" if @options[:allow_blank]
+ return true
+ end
+
+ super
+ end
+
+ # Returns a value to be used as new scope. It deals with four different
+ # cases: date, time, boolean and stringfiable (everything that can be
+ # converted to a string and the next value makes sense)
+ #
+ def new_value_for_scope(scope)
+ column_type = if @existing.respond_to?(:column_for_attribute)
+ @existing.column_for_attribute(scope)
+ else
+ nil
+ end
+
+ case column_type.type
+ when :int, :integer, :float, :decimal
+ new_value_for_stringfiable_scope(scope)
+ when :datetime, :timestamp, :time
+ Time.now + 10000
+ when :date
+ Date.today + 100
+ when :boolean
+ !@existing.send(scope)
+ else
+ new_value_for_stringfiable_scope(scope)
+ end
+ end
+
+ # Returns a value to be used as scope by generating a range of values
+ # and searching for them in the database.
+ #
+ def new_value_for_stringfiable_scope(scope)
+ values = [(@existing.send(scope) || 999).next.to_s]
+
+ # Generate a range of values to search in the database
+ 100.times do
+ values << values.last.next
+ end
+ conditions = { scope => values, @attribute => @value }
+
+ # Get values from the database, get the scope attribute and map them to string.
+ db_values = subject_class.find(:all, :conditions => conditions, :select => scope)
+ db_values.map!{ |r| r.send(scope).to_s }
+
+ if value_to_return = (values - db_values).first
+ value_to_return
+ else
+ raise ScriptError, "Tried to find an unique scope value for #{scope} but I could not. " <<
+ "The conditions hash was #{conditions.inspect} and it returned all records."
+ end
+ end
+ end
+
+ # Ensures that the model cannot be saved if one of the attributes listed
+ # is not unique.
+ #
+ # Requires an existing record in the database. If you supply :allow_nil as
+ # option, you need to have in the database a record which is not nil in the
+ # given attributes. The same is required for allow_blank option.
+ #
+ # Notice that the record being validate should not be the same as in the
+ # database. In other words, you can't do this:
+ #
+ # subject { Post.create!(@valid_attributes) }
+ # should_validate_uniqueness_of :title
+ #
+ # But don't worry, if you eventually do that, a helpful error message
+ # will be raised.
+ #
+ # == Options
+ #
+ # * <tt>:scope</tt> - field(s) to scope the uniqueness to.
+ # * <tt>:case_sensitive</tt> - the matcher look for an exact match.
+ # * <tt>:allow_nil</tt> - when supplied, validates if it allows nil or not.
+ # * <tt>:allow_blank</tt> - when supplied, validates if it allows blank or not.
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
+ # Regexp, string or symbol. Default = <tt>I18n.translate('activerecord.errors.messages.taken')</tt>
+ #
+ # == Examples
+ #
+ # it { should validate_uniqueness_of(:keyword, :username) }
+ # it { should validate_uniqueness_of(:email, :scope => :name, :case_sensitive => false) }
+ # it { should validate_uniqueness_of(:address, :scope => [:first_name, :last_name]) }
+ #
+ # should_validate_uniqueness_of :keyword, :username
+ # should_validate_uniqueness_of :email, :scope => :name, :case_sensitive => false
+ # should_validate_uniqueness_of :address, :scope => [:first_name, :last_name]
+ #
+ # should_validate_uniqueness_of :email do |m|
+ # m.scope = name
+ # m.case_sensitive = false
+ # end
+ #
+ def validates_is_unique(*attributes, &block)
+ ValidateUniquenessOfMatcher.new(*attributes, &block).spec(self)
+ end
+ end
+ end
+end
Oops, something went wrong.

0 comments on commit 61bd7d9

Please sign in to comment.