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.
- 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)
|