remarkable_activerecord 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. data/CHANGELOG +47 -0
  2. data/LICENSE +20 -0
  3. data/README +2 -0
  4. data/lib/remarkable_activerecord/base.rb +238 -0
  5. data/lib/remarkable_activerecord/human_names.rb +37 -0
  6. data/lib/remarkable_activerecord/matchers/allow_mass_assignment_of_matcher.rb +34 -0
  7. data/lib/remarkable_activerecord/matchers/allow_values_for_matcher.rb +94 -0
  8. data/lib/remarkable_activerecord/matchers/association_matcher.rb +235 -0
  9. data/lib/remarkable_activerecord/matchers/have_column_matcher.rb +68 -0
  10. data/lib/remarkable_activerecord/matchers/have_index_matcher.rb +57 -0
  11. data/lib/remarkable_activerecord/matchers/have_readonly_attributes_matcher.rb +30 -0
  12. data/lib/remarkable_activerecord/matchers/have_scope_matcher.rb +80 -0
  13. data/lib/remarkable_activerecord/matchers/validate_acceptance_of_matcher.rb +51 -0
  14. data/lib/remarkable_activerecord/matchers/validate_associated_matcher.rb +99 -0
  15. data/lib/remarkable_activerecord/matchers/validate_confirmation_of_matcher.rb +45 -0
  16. data/lib/remarkable_activerecord/matchers/validate_exclusion_of_matcher.rb +47 -0
  17. data/lib/remarkable_activerecord/matchers/validate_inclusion_of_matcher.rb +47 -0
  18. data/lib/remarkable_activerecord/matchers/validate_length_of_matcher.rb +123 -0
  19. data/lib/remarkable_activerecord/matchers/validate_numericality_of_matcher.rb +184 -0
  20. data/lib/remarkable_activerecord/matchers/validate_presence_of_matcher.rb +29 -0
  21. data/lib/remarkable_activerecord/matchers/validate_uniqueness_of_matcher.rb +151 -0
  22. data/lib/remarkable_activerecord.rb +29 -0
  23. data/locale/en.yml +253 -0
  24. data/spec/allow_mass_assignment_of_matcher_spec.rb +57 -0
  25. data/spec/allow_values_for_matcher_spec.rb +56 -0
  26. data/spec/association_matcher_spec.rb +616 -0
  27. data/spec/have_column_matcher_spec.rb +73 -0
  28. data/spec/have_index_matcher_spec.rb +68 -0
  29. data/spec/have_readonly_attributes_matcher_spec.rb +47 -0
  30. data/spec/have_scope_matcher_spec.rb +69 -0
  31. data/spec/model_builder.rb +101 -0
  32. data/spec/rcov.opts +2 -0
  33. data/spec/spec.opts +4 -0
  34. data/spec/spec_helper.rb +27 -0
  35. data/spec/validate_acceptance_of_matcher_spec.rb +68 -0
  36. data/spec/validate_associated_matcher_spec.rb +122 -0
  37. data/spec/validate_confirmation_of_matcher_spec.rb +58 -0
  38. data/spec/validate_exclusion_of_matcher_spec.rb +88 -0
  39. data/spec/validate_inclusion_of_matcher_spec.rb +84 -0
  40. data/spec/validate_length_of_matcher_spec.rb +165 -0
  41. data/spec/validate_numericality_of_matcher_spec.rb +180 -0
  42. data/spec/validate_presence_of_matcher_spec.rb +52 -0
  43. data/spec/validate_uniqueness_of_matcher_spec.rb +150 -0
  44. metadata +112 -0
@@ -0,0 +1,47 @@
1
+ require File.join(File.dirname(__FILE__), 'allow_values_for_matcher')
2
+
3
+ module Remarkable
4
+ module ActiveRecord
5
+ module Matchers
6
+ class ValidateExclusionOfMatcher < AllowValuesForMatcher
7
+
8
+ default_options :message => :exclusion
9
+
10
+ protected
11
+
12
+ def valid_values
13
+ @in_range ? [ @options[:in].first - 1, @options[:in].last + 1 ] : []
14
+ end
15
+
16
+ def invalid_values
17
+ @options[:in]
18
+ end
19
+
20
+ end
21
+
22
+ # Ensures that given values are not valid for the attribute. If a range
23
+ # is given, ensures that the attribute is not valid in the given range.
24
+ #
25
+ # == Options
26
+ #
27
+ # * <tt>:in</tt> - values to test exclusion.
28
+ # * <tt>:allow_nil</tt> - when supplied, validates if it allows nil or not.
29
+ # * <tt>:allow_blank</tt> - when supplied, validates if it allows blank or not.
30
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
31
+ # Regexp, string or symbol. Default = <tt>I18n.translate('activerecord.errors.messages.exclusion')</tt>
32
+ #
33
+ # == Examples
34
+ #
35
+ # it { should validate_exclusion_of(:username, :in => ["admin", "user"]) }
36
+ # it { should validate_exclusion_of(:age, :in => 30..60) }
37
+ #
38
+ # should_validate_exclusion_of :username, :in => ["admin", "user"]
39
+ # should_validate_exclusion_of :age, :in => 30..60
40
+ #
41
+ def validate_exclusion_of(*args)
42
+ ValidateExclusionOfMatcher.new(*args).spec(self)
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,47 @@
1
+ require File.join(File.dirname(__FILE__), 'allow_values_for_matcher')
2
+
3
+ module Remarkable
4
+ module ActiveRecord
5
+ module Matchers
6
+ class ValidateInclusionOfMatcher < AllowValuesForMatcher
7
+
8
+ default_options :message => :inclusion
9
+
10
+ protected
11
+
12
+ def valid_values
13
+ @options[:in]
14
+ end
15
+
16
+ def invalid_values
17
+ @in_range ? [ @options[:in].first - 1, @options[:in].last + 1 ] : []
18
+ end
19
+
20
+ end
21
+
22
+ # Ensures that given values are valid for the attribute. If a range
23
+ # is given, ensures that the attribute is valid in the given range.
24
+ #
25
+ # == Options
26
+ #
27
+ # * <tt>:in</tt> - values to test inclusion.
28
+ # * <tt>:allow_nil</tt> - when supplied, validates if it allows nil or not.
29
+ # * <tt>:allow_blank</tt> - when supplied, validates if it allows blank or not.
30
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
31
+ # Regexp, string or symbol. Default = <tt>I18n.translate('activerecord.errors.messages.inclusion')</tt>
32
+ #
33
+ # == Examples
34
+ #
35
+ # should_validate_inclusion_of :size, :in => ["S", "M", "L", "XL"]
36
+ # should_validate_inclusion_of :age, :in => 18..100
37
+ #
38
+ # it { should validate_inclusion_of(:size, :in => ["S", "M", "L", "XL"]) }
39
+ # it { should validate_inclusion_of(:age, :in => 18..100) }
40
+ #
41
+ def validate_inclusion_of(*args)
42
+ ValidateInclusionOfMatcher.new(*args).spec(self)
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,123 @@
1
+ module Remarkable
2
+ module ActiveRecord
3
+ module Matchers
4
+ class ValidateLengthOfMatcher < Remarkable::ActiveRecord::Base
5
+ arguments :collection => :attributes, :as => :attribute
6
+
7
+ optional :within, :alias => :in
8
+ optional :minimum, :maximum, :is
9
+ optional :allow_nil, :allow_blank, :default => true
10
+ optional :message, :too_short, :too_long, :wrong_length
11
+
12
+ collection_assertions :less_than_min_length?, :exactly_min_length?,
13
+ :more_than_max_length?, :exactly_max_length?,
14
+ :allow_nil?, :allow_blank?
15
+
16
+ before_assert do
17
+ # Reassign :in to :within
18
+ @options[:within] ||= @options.delete(:in) if @options.key?(:in)
19
+
20
+ if @options[:is]
21
+ @min_value, @max_value = @options[:is], @options[:is]
22
+ elsif @options[:within]
23
+ @min_value, @max_value = @options[:within].first, @options[:within].last
24
+ elsif @options[:maximum]
25
+ @min_value, @max_value = nil, @options[:maximum]
26
+ elsif @options[:minimum]
27
+ @min_value, @max_value = @options[:minimum], nil
28
+ end
29
+ end
30
+
31
+ default_options :too_short => :too_short, :too_long => :too_long, :wrong_length => :wrong_length
32
+
33
+ protected
34
+ def allow_nil?
35
+ super(default_message_for(:too_short))
36
+ end
37
+
38
+ def allow_blank?
39
+ super(default_message_for(:too_short))
40
+ end
41
+
42
+ def less_than_min_length?
43
+ @min_value.nil? || @min_value <= 1 || bad?(value_for_length(@min_value - 1), default_message_for(:too_short))
44
+ end
45
+
46
+ def exactly_min_length?
47
+ @min_value.nil? || @min_value <= 0 || good?(value_for_length(@min_value), default_message_for(:too_short))
48
+ end
49
+
50
+ def more_than_max_length?
51
+ @max_value.nil? || bad?(value_for_length(@max_value + 1), default_message_for(:too_long))
52
+ end
53
+
54
+ def exactly_max_length?
55
+ @max_value.nil? || @min_value == @max_value || good?(value_for_length(@max_value), default_message_for(:too_long))
56
+ end
57
+
58
+ def value_for_length(value)
59
+ "x" * value
60
+ end
61
+
62
+ def interpolation_options
63
+ { :minimum => @min_value, :maximum => @max_value }
64
+ end
65
+
66
+ # Returns the default message for the validation type.
67
+ # If user supplied :message, it will return it. Otherwise it will return
68
+ # wrong_length on :is validation and :too_short or :too_long in the other
69
+ # types.
70
+ #
71
+ def default_message_for(validation_type)
72
+ return :message if @options[:message]
73
+ @options.key?(:is) ? :wrong_length : validation_type
74
+ end
75
+ end
76
+
77
+ # Validates the length of the given attributes. You have also to supply
78
+ # one of the following options: minimum, maximum, is or within.
79
+ #
80
+ # Note: this method is also aliased as <tt>validate_size_of</tt>.
81
+ #
82
+ # == Options
83
+ #
84
+ # * <tt>:minimum</tt> - The minimum size of the attribute.
85
+ # * <tt>:maximum</tt> - The maximum size of the attribute.
86
+ # * <tt>:is</tt> - The exact size of the attribute.
87
+ # * <tt>:within</tt> - A range specifying the minimum and maximum size of the attribute.
88
+ # * <tt>:in</tt> - A synonym(or alias) for :within.
89
+ # * <tt>:allow_nil</tt> - when supplied, validates if it allows nil or not.
90
+ # * <tt>:allow_blank</tt> - when supplied, validates if it allows blank or not.
91
+ # * <tt>:too_short</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt> when attribute is too short.
92
+ # Regexp, string or symbol. Default = <tt>I18n.translate('activerecord.errors.messages.too_short') % range.first</tt>
93
+ # * <tt>:too_long</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt> when attribute is too long.
94
+ # Regexp, string or symbol. Default = <tt>I18n.translate('activerecord.errors.messages.too_long') % range.last</tt>
95
+ # * <tt>:wrong_length</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt> when attribute is the wrong length.
96
+ # Regexp, string or symbol. Default = <tt>I18n.translate('activerecord.errors.messages.wrong_length') % range.last</tt>
97
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
98
+ # Regexp, string or symbol. Default = <tt>I18n.translate('activerecord.errors.messages.wrong_length') % value</tt>
99
+ #
100
+ # == Gotcha
101
+ #
102
+ # In Rails 2.3.x, when :message is supplied, it overwrites the messages
103
+ # supplied in :wrong_length, :too_short and :too_long. However, in earlier
104
+ # versions, Rails ignores the :message option.
105
+ #
106
+ # == Examples
107
+ #
108
+ # should_validate_length_of :password, :within => 6..20
109
+ # should_validate_length_of :password, :maximum => 20
110
+ # should_validate_length_of :password, :minimum => 6
111
+ # should_validate_length_of :age, :is => 18
112
+ #
113
+ # it { should validate_length_of(:password).within(6..20) }
114
+ # it { should validate_length_of(:password).maximum(20) }
115
+ # it { should validate_length_of(:password).minimum(6) }
116
+ # it { should validate_length_of(:age).is(18) }
117
+ #
118
+ def validate_length_of(*attributes)
119
+ ValidateLengthOfMatcher.new(*attributes).spec(self)
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,184 @@
1
+ module Remarkable
2
+ module ActiveRecord
3
+ module Matchers
4
+ class ValidateNumericalityOfMatcher < Remarkable::ActiveRecord::Base
5
+
6
+ arguments :collection => :attributes, :as => :attribute
7
+
8
+ optional :equal_to, :greater_than_or_equal_to, :greater_than,
9
+ :less_than_or_equal_to, :less_than, :message
10
+
11
+ optional :only_integer, :odd, :even, :allow_nil, :allow_blank, :default => true
12
+
13
+ collection_assertions :only_numeric_values?, :allow_blank?, :allow_nil?,
14
+ :only_integer?, :only_odd?, :only_even?, :equals_to?,
15
+ :less_than_minimum?, :more_than_maximum?
16
+
17
+ # Before assertions, we rearrange the values.
18
+ #
19
+ # Notice that :less_than gives a maximum value while :more_than given
20
+ # a minimum value. While :equal_to generate both.
21
+ #
22
+ before_assert do
23
+ @maximum_values = {}
24
+ @minimum_values = {}
25
+
26
+ if value = @options[:equal_to]
27
+ @maximum_values[:equal_to] = value
28
+ @minimum_values[:equal_to] = value
29
+ elsif value = @options[:less_than]
30
+ @maximum_values[:less_than] = value - 1
31
+ elsif value = @options[:greater_than]
32
+ @minimum_values[:greater_than] = value + 1
33
+ elsif value = @options[:less_than_or_equal_to]
34
+ @maximum_values[:less_than_or_equal_to] = value
35
+ elsif value = @options[:greater_than_or_equal_to]
36
+ @minimum_values[:greater_than_or_equal_to] = value
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def allow_nil?
43
+ super(default_message_for(:not_a_number))
44
+ end
45
+
46
+ def allow_blank?
47
+ super(default_message_for(:not_a_number))
48
+ end
49
+
50
+ def only_numeric_values?
51
+ bad?("abcd", default_message_for(:not_a_number))
52
+ end
53
+
54
+ def only_integer?
55
+ assert_bad_or_good_if_key(:only_integer, valid_value_for_test.to_f, default_message_for(:not_a_number))
56
+ end
57
+
58
+ # In ActiveRecord, when we supply :even, does not matter the value, it
59
+ # considers that should even values should be accepted.
60
+ #
61
+ def only_even?
62
+ return true unless @options[:even]
63
+ bad?(even_valid_value_for_test + 1, default_message_for(:even))
64
+ end
65
+
66
+ # In ActiveRecord, when we supply :odd, does not matter the value, it
67
+ # considers that should odd values should be accepted.
68
+ #
69
+ def only_odd?
70
+ return true unless @options[:odd]
71
+ bad?(even_valid_value_for_test, default_message_for(:odd))
72
+ end
73
+
74
+ # Check equal_to for all registered values.
75
+ #
76
+ def equals_to?
77
+ values = {}
78
+ @maximum_values.each { |k, v| values[k] = v }
79
+ @minimum_values.each { |k, v| values[k] = v }
80
+
81
+ values.each do |key, value|
82
+ return false, :count => value unless good?(value, default_message_for(key))
83
+ end
84
+ true
85
+ end
86
+
87
+ # Check more_than_maximum? for equal_to, less_than and
88
+ # less_than_or_equal_to options.
89
+ #
90
+ def more_than_maximum?
91
+ @maximum_values.each do |key, value|
92
+ return false, :count => value unless bad?(value + 1, default_message_for(key))
93
+ end
94
+ true
95
+ end
96
+
97
+ # Check less_than_minimum? for equal_to, more_than and
98
+ # more_than_or_equal_to options.
99
+ #
100
+ def less_than_minimum?
101
+ @minimum_values.each do |key, value|
102
+ return false, :count => value unless bad?(value - 1, default_message_for(key))
103
+ end
104
+ true
105
+ end
106
+
107
+ # Returns a valid value for test.
108
+ #
109
+ def valid_value_for_test
110
+ value = @options[:equal_to] || @options[:less_than_or_equal_to] || @options[:greater_than_or_equal_to]
111
+
112
+ value ||= @options[:less_than] - 1 if @options[:less_than]
113
+ value ||= @options[:greater_than] + 1 if @options[:greater_than]
114
+
115
+ value ||= 10
116
+
117
+ if @options[:even]
118
+ value = (value / 2) * 2
119
+ elsif @options[:odd]
120
+ value = ((value / 2) * 2) + 1
121
+ end
122
+
123
+ value
124
+ end
125
+
126
+ # Returns a valid even value for test.
127
+ # The method valid_value_for_test checks for :even option but does not
128
+ # return necessarily an even value
129
+ #
130
+ def even_valid_value_for_test
131
+ (valid_value_for_test / 2) * 2
132
+ end
133
+
134
+ # Returns the default message for each key (:odd, :even, :equal_to, ...).
135
+ # If the user provided a message, we use it, otherwise we should use
136
+ # the given key as message.
137
+ #
138
+ # For example, a default_message_for(:odd), if none is provided, will be
139
+ # :odd. So we have create :odd_message in the options hash, that when
140
+ # called later, will return :odd.
141
+ #
142
+ def default_message_for(key)
143
+ if @options[:message]
144
+ :message
145
+ else
146
+ @options[:"#{key}_message"] = key
147
+ :"#{key}_message"
148
+ end
149
+ end
150
+ end
151
+
152
+ # Ensures that the given attributes accepts only numbers.
153
+ #
154
+ # == Options
155
+ #
156
+ # * <tt>:only_integer</tt> - when supplied, checks if it accepts only integers or not
157
+ # * <tt>:odd</tt> - when supplied, checks if it accepts only odd values or not
158
+ # * <tt>:even</tt> - when supplied, checks if it accepts only even values or not
159
+ # * <tt>:equal_to</tt> - when supplied, checks if attributes are only valid when equal to given value
160
+ # * <tt>:less_than</tt> - when supplied, checks if attributes are only valid when less than given value
161
+ # * <tt>:greater_than</tt> - when supplied, checks if attributes are only valid when greater than given value
162
+ # * <tt>:less_than_or_equal_to</tt> - when supplied, checks if attributes are only valid when less than or equal to given value
163
+ # * <tt>:greater_than_or_equal_to</tt> - when supplied, checks if attributes are only valid when greater than or equal to given value
164
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
165
+ # Regexp, string or symbol. Default = <tt>I18n.translate('activerecord.errors.messages.not_a_number')</tt>
166
+ #
167
+ # == Examples
168
+ #
169
+ # should_validate_numericality_of :age, :price
170
+ # should_validate_numericality_of :price, :only_integer => false, :greater_than => 10
171
+ #
172
+ # it { should validate_numericality_of(:age).odd }
173
+ # it { should validate_numericality_of(:age).even }
174
+ # it { should validate_numericality_of(:age).only_integer }
175
+ # it { should validate_numericality_of(:age, :odd => true) }
176
+ # it { should validate_numericality_of(:age, :even => true) }
177
+ #
178
+ def validate_numericality_of(*attributes)
179
+ ValidateNumericalityOfMatcher.new(*attributes).spec(self)
180
+ end
181
+
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,29 @@
1
+ module Remarkable
2
+ module ActiveRecord
3
+ module Matchers
4
+ class ValidatePresenceOfMatcher < Remarkable::ActiveRecord::Base
5
+ arguments :collection => :attributes, :as => :attribute
6
+ optional :message
7
+
8
+ collection_assertions :allow_nil?
9
+ default_options :message => :blank, :allow_nil => false
10
+ end
11
+
12
+ # Ensures that the model cannot be saved if one of the attributes listed is not present.
13
+ #
14
+ # == Options
15
+ #
16
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
17
+ # Regexp, string or symbol. Default = <tt>I18n.translate('activerecord.errors.messages.blank')</tt>
18
+ #
19
+ # == Examples
20
+ #
21
+ # should_validate_presence_of :name, :phone_number
22
+ # it { should validate_presence_of(:name, :phone_number) }
23
+ #
24
+ def validate_presence_of(*args)
25
+ ValidatePresenceOfMatcher.new(*args).spec(self)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,151 @@
1
+ module Remarkable
2
+ module ActiveRecord
3
+ module Matchers
4
+ class ValidateUniquenessOfMatcher < Remarkable::ActiveRecord::Base
5
+ arguments :collection => :attributes, :as => :attribute
6
+
7
+ optional :message
8
+ optional :scope, :splat => true
9
+ optional :case_sensitive, :allow_nil, :allow_blank, :default => true
10
+
11
+ collection_assertions :find_first_object?, :responds_to_scope?, :is_unique?, :case_sensitive?,
12
+ :valid_with_new_scope?, :allow_nil?, :allow_blank?
13
+
14
+ default_options :message => :taken
15
+
16
+ before_assert do
17
+ @options[:scope] = [*@options[:scope]].compact if @options[:scope]
18
+ end
19
+
20
+ private
21
+
22
+ # Tries to find an object in the database. If allow_nil and/or allow_blank
23
+ # is given, we must find a record which is not nil or not blank.
24
+ #
25
+ # If any of these attempts fail, the validation fail.
26
+ #
27
+ def find_first_object?
28
+ @existing, message = if @options[:allow_nil]
29
+ [ subject_class.find(:first, :conditions => "#{@attribute} IS NOT NULL"), " with #{@attribute} not nil" ]
30
+ elsif @options[:allow_blank]
31
+ [ subject_class.find(:first, :conditions => "#{@attribute} != ''"), " with #{@attribute} not blank" ]
32
+ else
33
+ [ subject_class.find(:first), "" ]
34
+ end
35
+
36
+ return true if @existing
37
+ raise ScriptError, "could not find a #{subject_class} in the database" + message
38
+ end
39
+
40
+ # Set subject scope to be equal to the object found.
41
+ #
42
+ def responds_to_scope?
43
+ (@options[:scope] || []).each do |scope|
44
+ method = :"#{scope}="
45
+ return false, :method => method unless @subject.respond_to?(method)
46
+
47
+ @subject.send(method, @existing.send(scope))
48
+ end
49
+ true
50
+ end
51
+
52
+ # Check if the attribute given is valid and if the validation fails for equal values.
53
+ #
54
+ def is_unique?
55
+ @value = @existing.send(@attribute)
56
+ return bad?(@value)
57
+ end
58
+
59
+ # If :case_sensitive is given and it's false, we swap the case of the
60
+ # value used in :is_unique? and see if the test object remains valid.
61
+ #
62
+ # If :case_sensitive is given and it's true, we swap the case of the
63
+ # value used in is_unique? and see if the test object is not valid.
64
+ #
65
+ # This validation will only occur if the test object is a String.
66
+ #
67
+ def case_sensitive?
68
+ return true unless @value.is_a?(String)
69
+ assert_good_or_bad_if_key(:case_sensitive, @value.swapcase)
70
+ end
71
+
72
+ # Now test that the object is valid when changing the scoped attribute.
73
+ #
74
+ def valid_with_new_scope?
75
+ (@options[:scope] || []).each do |scope|
76
+ previous_scope_value = @subject.send(scope)
77
+
78
+ @subject.send("#{scope}=", new_value_for_scope(scope))
79
+ return false, :method => scope unless good?(@value)
80
+
81
+ @subject.send("#{scope}=", previous_scope_value)
82
+ end
83
+ true
84
+ end
85
+
86
+ # Change the existing object attribute to nil to run allow nil validation.
87
+ #
88
+ def allow_nil?
89
+ @existing.update_attribute(@attribute, nil)
90
+ super
91
+ end
92
+
93
+ # Change the existing object attribute to blank to run allow blank validation.
94
+ #
95
+ def allow_blank?
96
+ @existing.update_attribute(@attribute, '')
97
+ super
98
+ end
99
+
100
+ # Returns a value to be used as new scope. It does a range query in the
101
+ # database and tries to return a new value which does not belong to it.
102
+ #
103
+ def new_value_for_scope(scope)
104
+ new_scope = (@existing.send(scope) || 999).next.to_s
105
+
106
+ # Generate a range of values to search in the database
107
+ values = 100.times.inject([new_scope]) {|v,i| v << v.last.next }
108
+ conditions = { scope => values, @attribute => @value }
109
+
110
+ # Get values from the database, get the scope attribute and map them to string.
111
+ db_values = subject_class.find(:all, :conditions => conditions, :select => scope)
112
+ db_values.map!{ |r| r.send(scope).to_s }
113
+
114
+ if value_to_return = (values - db_values).first
115
+ value_to_return
116
+ else
117
+ raise ScriptError, "Tried to find an unique scope value for #{scope} but I could not. " <<
118
+ "The conditions hash was #{conditions.inspect} and it returned all records."
119
+ end
120
+ end
121
+ end
122
+
123
+ # Ensures that the model cannot be saved if one of the attributes listed
124
+ # is not unique.
125
+ #
126
+ # Requires an existing record in the database. If you supply :allow_nil as
127
+ # option, you need to have in the database a record which is not nil in the
128
+ # given attributes. The same is required for allow_blank option.
129
+ #
130
+ # == Options
131
+ #
132
+ # * <tt>:scope</tt> - field(s) to scope the uniqueness to.
133
+ # * <tt>:case_sensitive</tt> - the matcher look for an exact match.
134
+ # * <tt>:allow_nil</tt> - when supplied, validates if it allows nil or not.
135
+ # * <tt>:allow_blank</tt> - when supplied, validates if it allows blank or not.
136
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
137
+ # Regexp, string or symbol. Default = <tt>I18n.translate('activerecord.errors.messages.taken')</tt>
138
+ #
139
+ # == Examples
140
+ #
141
+ # it { should validate_uniqueness_of(:keyword, :username) }
142
+ # it { should validate_uniqueness_of(:name, :message => "O NOES! SOMEONE STOELED YER NAME!") }
143
+ # it { should validate_uniqueness_of(:email, :scope => :name, :case_sensitive => false) }
144
+ # it { should validate_uniqueness_of(:address, :scope => [:first_name, :last_name]) }
145
+ #
146
+ def validate_uniqueness_of(*attributes)
147
+ ValidateUniquenessOfMatcher.new(*attributes).spec(self)
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,29 @@
1
+ # Load Remarkable
2
+ unless Object.const_defined?('Remarkable')
3
+ begin
4
+ require 'remarkable'
5
+ rescue LoadError
6
+ require 'rubygems'
7
+ gem 'remarkable'
8
+ require 'remarkable'
9
+ end
10
+ end
11
+
12
+ # Load Remarkable ActiveRecord files
13
+ dir = File.dirname(__FILE__)
14
+ require File.join(dir, 'remarkable_activerecord', 'base')
15
+ require File.join(dir, 'remarkable_activerecord', 'human_names')
16
+
17
+ # Add locale
18
+ Remarkable.add_locale File.join(dir, '..', 'locale', 'en.yml')
19
+
20
+ # Add matchers
21
+ Dir[File.join(dir, 'remarkable_activerecord', 'matchers', '*.rb')].each do |file|
22
+ require file
23
+ end
24
+
25
+ # By default, ActiveRecord matchers are not included in any example group.
26
+ # The responsable for this is RemarkableRails. If you are using ActiveRecord
27
+ # without Rails, put the line below in your spec_helper to include ActiveRecord
28
+ # matchers into rspec globally.
29
+ # Remarkable.include_matchers!(Remarkable::ActiveRecord, Spec::Example::ExampleGroup)