remarkable_activerecord 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +47 -0
- data/LICENSE +20 -0
- data/README +2 -0
- data/lib/remarkable_activerecord/base.rb +238 -0
- data/lib/remarkable_activerecord/human_names.rb +37 -0
- data/lib/remarkable_activerecord/matchers/allow_mass_assignment_of_matcher.rb +34 -0
- data/lib/remarkable_activerecord/matchers/allow_values_for_matcher.rb +94 -0
- data/lib/remarkable_activerecord/matchers/association_matcher.rb +235 -0
- data/lib/remarkable_activerecord/matchers/have_column_matcher.rb +68 -0
- data/lib/remarkable_activerecord/matchers/have_index_matcher.rb +57 -0
- data/lib/remarkable_activerecord/matchers/have_readonly_attributes_matcher.rb +30 -0
- data/lib/remarkable_activerecord/matchers/have_scope_matcher.rb +80 -0
- data/lib/remarkable_activerecord/matchers/validate_acceptance_of_matcher.rb +51 -0
- data/lib/remarkable_activerecord/matchers/validate_associated_matcher.rb +99 -0
- data/lib/remarkable_activerecord/matchers/validate_confirmation_of_matcher.rb +45 -0
- data/lib/remarkable_activerecord/matchers/validate_exclusion_of_matcher.rb +47 -0
- data/lib/remarkable_activerecord/matchers/validate_inclusion_of_matcher.rb +47 -0
- data/lib/remarkable_activerecord/matchers/validate_length_of_matcher.rb +123 -0
- data/lib/remarkable_activerecord/matchers/validate_numericality_of_matcher.rb +184 -0
- data/lib/remarkable_activerecord/matchers/validate_presence_of_matcher.rb +29 -0
- data/lib/remarkable_activerecord/matchers/validate_uniqueness_of_matcher.rb +151 -0
- data/lib/remarkable_activerecord.rb +29 -0
- data/locale/en.yml +253 -0
- data/spec/allow_mass_assignment_of_matcher_spec.rb +57 -0
- data/spec/allow_values_for_matcher_spec.rb +56 -0
- data/spec/association_matcher_spec.rb +616 -0
- data/spec/have_column_matcher_spec.rb +73 -0
- data/spec/have_index_matcher_spec.rb +68 -0
- data/spec/have_readonly_attributes_matcher_spec.rb +47 -0
- data/spec/have_scope_matcher_spec.rb +69 -0
- data/spec/model_builder.rb +101 -0
- data/spec/rcov.opts +2 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/validate_acceptance_of_matcher_spec.rb +68 -0
- data/spec/validate_associated_matcher_spec.rb +122 -0
- data/spec/validate_confirmation_of_matcher_spec.rb +58 -0
- data/spec/validate_exclusion_of_matcher_spec.rb +88 -0
- data/spec/validate_inclusion_of_matcher_spec.rb +84 -0
- data/spec/validate_length_of_matcher_spec.rb +165 -0
- data/spec/validate_numericality_of_matcher_spec.rb +180 -0
- data/spec/validate_presence_of_matcher_spec.rb +52 -0
- data/spec/validate_uniqueness_of_matcher_spec.rb +150 -0
- 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)
|