Browse files

Add allow_nil option to the validate uniquness matcher

  • Loading branch information...
1 parent 8d93d9d commit 5da06714de194988707a09ad0551f0bcaa1e4b2a @drapergeek drapergeek committed with mxie Nov 2, 2012
View
2 NEWS.md
@@ -1,5 +1,7 @@
# HEAD
+* Add `allow_nil` option to the `validate_uniqueness_of` matcher
+
# v 2.0.0
* Remove the following matchers:
* `assign_to`
View
69 lib/shoulda/matchers/active_model/validate_uniqueness_of_matcher.rb
@@ -55,12 +55,26 @@ def case_insensitive
self
end
+ def allow_nil
+ @options[:allow_nil] = true
+ self
+ end
+
+ def description
+ result = "require "
+ result << "case sensitive " unless @options[:case_insensitive]
+ result << "unique value for #{@attribute}"
+ result << " scoped to #{@options[:scopes].join(', ')}" if @options[:scopes].present?
+ result
+ end
+
def matches?(subject)
@subject = subject.class.new
@expected_message ||= :taken
set_scoped_attributes &&
- validate_attribute? &&
- validate_after_scope_change?
+ validate_everything_except_duplicate_nils? &&
+ validate_after_scope_change? &&
+ allows_nil?
end
def description
@@ -73,17 +87,42 @@ def description
private
- def existing
- @existing ||= first_instance
+ def allows_nil?
+ if @options[:allow_nil]
+ ensure_nil_record_in_database
+ allows_value_of(nil, @expected_message)
+ else
+ true
+ end
+ end
+
+ def existing_record
+ @existing_record ||= first_instance
end
def first_instance
- @subject.class.first || create_instance_in_database
+ @subject.class.first || create_record_in_database
+ end
+
+ def ensure_nil_record_in_database
+ unless existing_record_is_nil?
+ create_record_in_database(nil_value: true)
+ end
end
- def create_instance_in_database
+ def existing_record_is_nil?
+ @existing_record.present? && existing_value.nil?
+ end
+
+ def create_record_in_database(options = {})
+ if options[:nil_value]
+ value = nil
+ else
+ value = "arbitrary_string"
+ end
+
@subject.class.new.tap do |instance|
- instance.send("#{@attribute}=", 'arbitrary_string')
+ instance.send("#{@attribute}=", value)
instance.save(:validate => false)
end
end
@@ -93,7 +132,7 @@ def set_scoped_attributes
@options[:scopes].all? do |scope|
setter = :"#{scope}="
if @subject.respond_to?(setter)
- @subject.send(setter, existing.send(scope))
+ @subject.send(setter, existing_record.send(scope))
true
else
@failure_message_for_should = "#{class_name} doesn't seem to have a #{scope} attribute."
@@ -105,10 +144,18 @@ def set_scoped_attributes
end
end
- def validate_attribute?
+ def validate_everything_except_duplicate_nils?
+ if @options[:allow_nil] && existing_value.nil?
+ create_record_without_nil
+ end
+
disallows_value_of(existing_value, @expected_message)
end
+ def create_record_without_nil
+ @existing_record = create_record_in_database
+ end
+
# TODO: There is a chance that we could change the scoped field
# to a value that's already taken. An alternative implementation
# could actually find all values for scope and create a unique
@@ -117,7 +164,7 @@ def validate_after_scope_change?
true
else
@options[:scopes].all? do |scope|
- previous_value = existing.send(scope)
+ previous_value = existing_record.send(scope)
# Assume the scope is a foreign key if the field is nil
previous_value ||= correct_type_for_column(@subject.class.columns_hash[scope.to_s])
@@ -157,7 +204,7 @@ def class_name
end
def existing_value
- value = existing.send(@attribute)
+ value = existing_record.send(@attribute)
if @options[:case_insensitive] && value.respond_to?(:swapcase!)
value.swapcase!
end
View
195 spec/shoulda/active_model/validate_uniqueness_of_matcher_spec.rb
@@ -0,0 +1,195 @@
+require 'spec_helper'
+
+describe Shoulda::Matchers::ActiveModel::ValidateUniquenessOfMatcher do
+ context "a unique attribute" do
+ before do
+ @model = define_model(:example, :attr => :string,
+ :other => :integer) do
+ attr_accessible :attr, :other
+ validates_uniqueness_of :attr
+ end.new
+ end
+
+ context "with an existing value" do
+ before do
+ @existing = Example.create!(:attr => 'value', :other => 1)
+ end
+
+ it "should require a unique value for that attribute" do
+ @model.should validate_uniqueness_of(:attr)
+ end
+
+ it "should pass when the subject is an existing record" do
+ @existing.should validate_uniqueness_of(:attr)
+ end
+
+ it "should fail when a scope is specified" do
+ @model.should_not validate_uniqueness_of(:attr).scoped_to(:other)
+ end
+ end
+
+ context "without an existing value" do
+ before do
+ Example.first.should be_nil
+ @matcher = validate_uniqueness_of(:attr)
+ end
+
+ it "does not not require a created instance" do
+ @model.should @matcher
+ end
+ end
+ end
+
+ context "a unique attribute with a custom error and an existing value" do
+ before do
+ @model = define_model(:example, :attr => :string) do
+ attr_accessible :attr
+ validates_uniqueness_of :attr, :message => 'Bad value'
+ end.new
+ Example.create!(:attr => 'value')
+ end
+
+ it "should fail when checking the default message" do
+ @model.should_not validate_uniqueness_of(:attr)
+ end
+
+ it "should fail when checking a message that doesn't match" do
+ @model.should_not validate_uniqueness_of(:attr).with_message(/abc/i)
+ end
+
+ it "should pass when checking a message that matches" do
+ @model.should validate_uniqueness_of(:attr).with_message(/bad/i)
+ end
+ end
+
+ context "a scoped unique attribute with an existing value" do
+ before do
+ @model = define_model(:example, :attr => :string,
+ :scope1 => :integer,
+ :scope2 => :integer,
+ :other => :integer) do
+ attr_accessible :attr, :scope1, :scope2, :other
+ validates_uniqueness_of :attr, :scope => [:scope1, :scope2]
+ end.new
+ @existing = Example.create!(:attr => 'value', :scope1 => 1, :scope2 => 2, :other => 3)
+ end
+
+ it "should pass when the correct scope is specified" do
+ @model.should validate_uniqueness_of(:attr).scoped_to(:scope1, :scope2)
+ end
+
+ it "should pass when the subject is an existing record" do
+ @existing.should validate_uniqueness_of(:attr).scoped_to(:scope1, :scope2)
+ end
+
+ it "should fail when too narrow of a scope is specified" do
+ @model.should_not validate_uniqueness_of(:attr).scoped_to(:scope1, :scope2, :other)
+ end
+
+ it "should fail when too broad of a scope is specified" do
+ @model.should_not validate_uniqueness_of(:attr).scoped_to(:scope1)
+ end
+
+ it "should fail when a different scope is specified" do
+ @model.should_not validate_uniqueness_of(:attr).scoped_to(:other)
+ end
+
+ it "should fail when no scope is specified" do
+ @model.should_not validate_uniqueness_of(:attr)
+ end
+
+ it "should fail when a non-existent attribute is specified as a scope" do
+ @model.should_not validate_uniqueness_of(:attr).scoped_to(:fake)
+ end
+ end
+
+ context "a non-unique attribute with an existing value" do
+ before do
+ @model = define_model(:example, :attr => :string) do
+ attr_accessible :attr
+ end.new
+ Example.create!(:attr => 'value')
+ end
+
+ it "should not require a unique value for that attribute" do
+ @model.should_not validate_uniqueness_of(:attr)
+ end
+ end
+
+ context "a case sensitive unique attribute with an existing value" do
+ before do
+ @model = define_model(:example, :attr => :string) do
+ attr_accessible :attr
+ validates_uniqueness_of :attr, :case_sensitive => true
+ end.new
+ Example.create!(:attr => 'value')
+ end
+
+ it "should not require a unique, case-insensitive value for that attribute" do
+ @model.should_not validate_uniqueness_of(:attr).case_insensitive
+ end
+
+ it "should require a unique, case-sensitive value for that attribute" do
+ @model.should validate_uniqueness_of(:attr)
+ end
+ end
+
+ context "a case sensitive unique integer attribute with an existing value" do
+ before do
+ @model = define_model(:example, :attr => :integer) do
+ attr_accessible :attr
+ validates_uniqueness_of :attr, :case_sensitive => true
+ end.new
+ Example.create!(:attr => 'value')
+ end
+
+ it "should require a unique, case-insensitive value for that attribute" do
+ @model.should validate_uniqueness_of(:attr).case_insensitive
+ end
+
+ it "should require a unique, case-sensitive value for that attribute" do
+ @model.should validate_uniqueness_of(:attr)
+ end
+ end
+
+ context "when the validation allows nil" do
+ before do
+ @model = define_model(:example, :attr => :integer) do
+ attr_accessible :attr
+ validates_uniqueness_of :attr, :allow_nil => true
+ end.new
+ end
+
+ context "when there is an existing entry with a nil" do
+ it "should allow_nil" do
+ Example.create!(:attr => nil)
+ @model.should validate_uniqueness_of(:attr).allow_nil
+ end
+ end
+
+ it "should create a nil and verify that it is allowed" do
+ @model.should validate_uniqueness_of(:attr).allow_nil
+ Example.all.any?{ |instance| instance.attr.nil? }
+ end
+ end
+
+ context "when the validation does not allow a nil" do
+ before do
+ @model = define_model(:example, :attr => :integer) do
+ attr_accessible :attr
+ validates_uniqueness_of :attr
+ end.new
+ end
+
+ context "when there is an existing entry with a nil" do
+ it "should not allow_nil" do
+ Example.create!(:attr => nil)
+ @model.should_not validate_uniqueness_of(:attr).allow_nil
+ end
+ end
+
+ it "should not allow_nil" do
+ @model.should_not validate_uniqueness_of(:attr).allow_nil
+ end
+ end
+end

0 comments on commit 5da0671

Please sign in to comment.