remarkable_activerecord 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
data/CHANGELOG ADDED
@@ -0,0 +1,47 @@
1
+ # v3.0.0
2
+
3
+ [ENHANCEMENT] Added more options to associations matcher. Previously it was
4
+ handling just :dependent and :through options. Now it deals with:
5
+
6
+ :through, :class_name, :foreign_key, :dependent, :join_table, :uniq,
7
+ :readonly, :validate, :autosave, :counter_cache, :polymorphic
8
+
9
+ And they are much smarter! In :join_table and :through cases, they also test if
10
+ the table exists or not. :counter_cache and :foreign_key also checks if the
11
+ column exists or not.
12
+
13
+ [COMPATIBILITY] Removed callback, have_instance_method and have_class_method
14
+ matchers. They don't lead to a good TDD since you should test they behavior
15
+ and not wether they exist or not.
16
+
17
+ [COMPATIBILITY] ActiveRecord matches does not pick the instance variable from
18
+ the spec environment. So we should target only rspec versions that supports
19
+ subjects (>= 1.1.12).
20
+
21
+ Previously, when we are doing this:
22
+
23
+ describe Product
24
+ before(:each){ @product = Product.new(:tangible => true) }
25
+ should_validate_presence_of :size
26
+ end
27
+
28
+ It was validating the @product instance variable. However this might be not
29
+ clear. The right way to do that (with subjects) is:
30
+
31
+ describe Product
32
+ subject{ Product.new(:tangible => true) }
33
+ should_validate_presence_of :size
34
+ end
35
+
36
+ Is also valid to remember that previous versions of Remarkable were overriding
37
+ subject definitions on rspec. This was also fixed.
38
+
39
+ # v2.x
40
+
41
+ [ENHANCEMENT] Added associations, allow_mass_assignment, allow_values_for,
42
+ have_column, have_index, have_scope, have_readonly_attributes,
43
+ validate_acceptance_of, validate_associate, validate_confirmation_of,
44
+ validate_exclusion_of, validate_inclusion_of, validate_length_of,
45
+ validate_numericality_of, validate_presence_of and validate_uniqueness_of
46
+ matchers.
47
+
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Carlos Brando
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,2 @@
1
+ Remarkable ActiveRecord
2
+ =======================
@@ -0,0 +1,238 @@
1
+ module Remarkable
2
+ module ActiveRecord
3
+ class Base < Remarkable::Base
4
+
5
+ def with_options(opts={})
6
+ @options.merge!(opts)
7
+ self
8
+ end
9
+
10
+ protected
11
+
12
+ # Checks for the given key in @options, if it exists and it's true,
13
+ # tests that the value is bad, otherwise tests that the value is good.
14
+ #
15
+ # It accepts the key to check for, the value that is used for testing
16
+ # and an @options key where the message to search for is.
17
+ #
18
+ def assert_bad_or_good_if_key(key, value, message_key=:message) #:nodoc:
19
+ return true unless @options.key?(key)
20
+
21
+ if @options[key]
22
+ return true if bad?(value, message_key)
23
+ return false, :not => not_word
24
+ else
25
+ return true if good?(value, message_key)
26
+ return false, :not => ''
27
+ end
28
+ end
29
+
30
+ # Checks for the given key in @options, if it exists and it's true,
31
+ # tests that the value is good, otherwise tests that the value is bad.
32
+ #
33
+ # It accepts the key to check for, the value that is used for testing
34
+ # and an @options key where the message to search for is.
35
+ #
36
+ def assert_good_or_bad_if_key(key, value, message_key=:message) #:nodoc:
37
+ return true unless @options.key?(key)
38
+
39
+ if @options[key]
40
+ return true if good?(value, message_key)
41
+ return false, :not => ''
42
+ else
43
+ return true if bad?(value, message_key)
44
+ return false, :not => not_word
45
+ end
46
+ end
47
+
48
+ # Default allow_nil? validation. It accepts the message_key which is
49
+ # the key which contain the message in @options.
50
+ #
51
+ # It also gets an allow_nil message on remarkable.active_record.allow_nil
52
+ # to be used as default.
53
+ #
54
+ def allow_nil?(message_key=:message) #:nodoc:
55
+ valid, options = assert_good_or_bad_if_key(:allow_nil, nil, message_key)
56
+
57
+ unless valid
58
+ default = Remarkable.t "remarkable.active_record.allow_nil", default_i18n_options.except(:scope).merge(options)
59
+ return false, options.merge(:default => default)
60
+ end
61
+
62
+ true
63
+ end
64
+
65
+ # Default allow_blank? validation. It accepts the message_key which is
66
+ # the key which contain the message in @options.
67
+ #
68
+ # It also gets an allow_blank message on remarkable.active_record.allow_blank
69
+ # to be used as default.
70
+ #
71
+ def allow_blank?(message_key=:message) #:nodoc:
72
+ valid, options = assert_good_or_bad_if_key(:allow_blank, '', message_key)
73
+
74
+ unless valid
75
+ default = Remarkable.t "remarkable.active_record.allow_blank", default_i18n_options.except(:scope).merge(options)
76
+ return false, options.merge(:default => default)
77
+ end
78
+
79
+ true
80
+ end
81
+
82
+ # Shortcut for assert_good_value.
83
+ #
84
+ def good?(value, message_sym=:message) #:nodoc:
85
+ assert_good_value(@subject, @attribute, value, @options[message_sym])
86
+ end
87
+
88
+ # Shortcut for assert_bad_value.
89
+ #
90
+ def bad?(value, message_sym=:message) #:nodoc:
91
+ assert_bad_value(@subject, @attribute, value, @options[message_sym])
92
+ end
93
+
94
+ # Asserts that an Active Record model validates with the passed
95
+ # <tt>value</tt> by making sure the <tt>error_message_to_avoid</tt> is not
96
+ # contained within the list of errors for that attribute.
97
+ #
98
+ # assert_good_value(User.new, :email, "user@example.com")
99
+ # assert_good_value(User.new, :ssn, "123456789", /length/)
100
+ #
101
+ # If a class is passed as the first argument, a new object will be
102
+ # instantiated before the assertion. If an instance variable exists with
103
+ # the same name as the class (underscored), that object will be used
104
+ # instead.
105
+ #
106
+ # assert_good_value(User, :email, "user@example.com")
107
+ #
108
+ # @product = Product.new(:tangible => false)
109
+ # assert_good_value(Product, :price, "0")
110
+ #
111
+ def assert_good_value(model, attribute, value, error_message_to_avoid=//) # :nodoc:
112
+ model.send("#{attribute}=", value)
113
+
114
+ return true if model.valid?
115
+
116
+ error_message_to_avoid = error_message_from_model(model, attribute, error_message_to_avoid)
117
+ assert_does_not_contain(model.errors.on(attribute), error_message_to_avoid)
118
+ end
119
+
120
+ # Asserts that an Active Record model invalidates the passed
121
+ # <tt>value</tt> by making sure the <tt>error_message_to_expect</tt> is
122
+ # contained within the list of errors for that attribute.
123
+ #
124
+ # assert_bad_value(User.new, :email, "invalid")
125
+ # assert_bad_value(User.new, :ssn, "123", /length/)
126
+ #
127
+ # If a class is passed as the first argument, a new object will be
128
+ # instantiated before the assertion. If an instance variable exists with
129
+ # the same name as the class (underscored), that object will be used
130
+ # instead.
131
+ #
132
+ # assert_bad_value(User, :email, "invalid")
133
+ #
134
+ # @product = Product.new(:tangible => true)
135
+ # assert_bad_value(Product, :price, "0")
136
+ #
137
+ def assert_bad_value(model, attribute, value, error_message_to_expect=:invalid) #:nodoc:
138
+ model.send("#{attribute}=", value)
139
+
140
+ return false if model.valid? || model.errors.on(attribute).blank?
141
+
142
+ error_message_to_expect = error_message_from_model(model, attribute, error_message_to_expect)
143
+ assert_contains(model.errors.on(attribute), error_message_to_expect)
144
+ end
145
+
146
+ # Return the error message to be checked. If the message is not a Symbol
147
+ # neither a Hash, it returns the own message.
148
+ #
149
+ # But the nice thing is that when the message is a Symbol we get the error
150
+ # messsage from within the model, using already existent structure inside
151
+ # ActiveRecord.
152
+ #
153
+ # This allows a couple things from the user side:
154
+ #
155
+ # 1. Specify symbols in their tests:
156
+ #
157
+ # should_allow_values_for(:shirt_size, 'S', 'M', 'L', :message => :inclusion)
158
+ #
159
+ # As we know, allow_values_for searches for a :invalid message. So if we
160
+ # were testing a validates_inclusion_of with allow_values_for, previously
161
+ # we had to do something like this:
162
+ #
163
+ # should_allow_values_for(:shirt_size, 'S', 'M', 'L', :message => 'not included in list')
164
+ #
165
+ # Now everything gets resumed to a Symbol.
166
+ #
167
+ # 2. Do not worry with specs if their are using I18n API properly.
168
+ #
169
+ # As we know, I18n API provides several interpolation options besides
170
+ # fallback when creating error messages. If the user changed the message,
171
+ # macros would start to pass when they shouldn't.
172
+ #
173
+ # Using the underlying mechanism inside ActiveRecord makes us free from
174
+ # all thos errors.
175
+ #
176
+ # We replace {{count}} interpolation for 12345 which later is replaced
177
+ # by a regexp which contains \d+.
178
+ #
179
+ def error_message_from_model(model, attribute, message) #:nodoc:
180
+ if message.is_a? Symbol
181
+ message = if RAILS_I18N # Rails >= 2.2
182
+ model.errors.generate_message(attribute, message, :count => '12345')
183
+ else # Rails <= 2.1
184
+ ::ActiveRecord::Errors.default_error_messages[message] % '12345'
185
+ end
186
+
187
+ if message =~ /12345/
188
+ message = Regexp.escape(message)
189
+ message.gsub!('12345', '\d+')
190
+ message = /#{message}/
191
+ end
192
+ end
193
+
194
+ message
195
+ end
196
+
197
+ # Asserts that the given collection does not contain item x. If x is a
198
+ # regular expression, ensure that none of the elements from the collection
199
+ # match x.
200
+ #
201
+ def assert_does_not_contain(collection, x) #:nodoc:
202
+ !assert_contains(collection, x)
203
+ end
204
+
205
+ # Changes how collection are interpolated to provide localized names
206
+ # whenever is possible.
207
+ #
208
+ def collection_interpolation
209
+ described_class = if @subject
210
+ subject_class
211
+ elsif @spec
212
+ @spec.send(:described_class)
213
+ end
214
+
215
+ if RAILS_I18N && described_class.respond_to?(:human_attribute_name) && self.class.matcher_arguments[:collection]
216
+ options = {}
217
+
218
+ collection_name = self.class.matcher_arguments[:collection].to_sym
219
+ if collection = instance_variable_get("@#{collection_name}")
220
+ collection.map!{|attr| described_class.human_attribute_name(attr.to_s, :locale => Remarkable.locale).downcase }
221
+ options[collection_name] = array_to_sentence(collection)
222
+ end
223
+
224
+ object_name = self.class.matcher_arguments[:as]
225
+ if object = instance_variable_get("@#{object_name}")
226
+ object = described_class.human_attribute_name(object.to_s, :locale => Remarkable.locale).downcase
227
+ options[object_name] = object
228
+ end
229
+
230
+ options
231
+ else
232
+ super
233
+ end
234
+ end
235
+
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,37 @@
1
+ if defined?(Spec)
2
+ module Spec
3
+ module Example
4
+ module ExampleGroupMethods
5
+
6
+ # This allows "describe User" to use the I18n human name of User.
7
+ #
8
+ def self.build_description_with_i18n(*args)
9
+ args.inject("") do |description, arg|
10
+ arg = if RAILS_I18N && arg.respond_to?(:human_name)
11
+ arg.human_name(:locale => Remarkable.locale)
12
+ else
13
+ arg.to_s
14
+ end
15
+
16
+ description << " " unless (description == "" || arg =~ /^(\s|\.|#)/)
17
+ description << arg
18
+ end
19
+ end
20
+
21
+ # This is for rspec <= 1.1.12.
22
+ #
23
+ def self.description_text(*args)
24
+ self.build_description_with_i18n(*args)
25
+ end
26
+
27
+ # This is for rspec >= 1.2.0.
28
+ #
29
+ def build_description_from(*args)
30
+ text = ExampleGroupMethods.build_description_with_i18n(*args)
31
+ text == "" ? nil : text
32
+ end
33
+
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,34 @@
1
+ module Remarkable
2
+ module ActiveRecord
3
+ module Matchers
4
+ class AllowMassAssignmentOfMatcher < Remarkable::ActiveRecord::Base
5
+ arguments :collection => :attributes, :as => :attribute
6
+
7
+ collection_assertions :is_protected?, :is_accessible?
8
+
9
+ protected
10
+
11
+ def is_protected?
12
+ protected = subject_class.protected_attributes || []
13
+ protected.empty? || !protected.include?(@attribute.to_s)
14
+ end
15
+
16
+ def is_accessible?
17
+ accessible = subject_class.accessible_attributes || []
18
+ accessible.empty? || accessible.include?(@attribute.to_s)
19
+ end
20
+ end
21
+
22
+ # Ensures that the attribute can be set on mass update.
23
+ #
24
+ # == Examples
25
+ #
26
+ # should_allow_mass_assignment_of :email, :name
27
+ # it { should allow_mass_assignment_of(:email, :name) }
28
+ #
29
+ def allow_mass_assignment_of(*attributes)
30
+ AllowMassAssignmentOfMatcher.new(*attributes).spec(self)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,94 @@
1
+ module Remarkable
2
+ module ActiveRecord
3
+ module Matchers
4
+ class AllowValuesForMatcher < Remarkable::ActiveRecord::Base
5
+ arguments :collection => :attributes, :as => :attribute
6
+
7
+ optional :message
8
+ optional :in, :splat => true
9
+ optional :allow_nil, :allow_blank, :default => true
10
+
11
+ collection_assertions :is_valid?, :is_invalid?, :allow_nil?, :allow_blank?
12
+
13
+ default_options :message => :invalid
14
+
15
+ before_assert do
16
+ first_value = @options[:in].is_a?(Array) ? @options[:in].first : @options[:in]
17
+ @in_range = first_value.is_a?(Range)
18
+
19
+ @options[:in] = if @in_range
20
+ first_value.to_a[0,2] + first_value.to_a[-2,2]
21
+ else
22
+ [*@options[:in]].compact
23
+ end
24
+
25
+ @options[:in].uniq!
26
+ end
27
+
28
+ protected
29
+
30
+ def is_valid?
31
+ valid_values.each do |value|
32
+ return false, :value => value.inspect unless good?(value)
33
+ end
34
+ true
35
+ end
36
+
37
+ def is_invalid?
38
+ invalid_values.each do |value|
39
+ return false, :value => value.inspect unless bad?(value)
40
+ end
41
+ true
42
+ end
43
+
44
+ def valid_values
45
+ @options[:in]
46
+ end
47
+
48
+ def invalid_values
49
+ []
50
+ end
51
+
52
+ def interpolation_options
53
+ options = if @in_range
54
+ { :in => (@options[:in].first..@options[:in].last).inspect }
55
+ elsif @options[:in].is_a?(Array)
56
+ { :in => @options[:in].map(&:inspect).to_sentence }
57
+ else
58
+ { :in => @options[:in].inspect }
59
+ end
60
+
61
+ options.merge!(:behavior => @behavior.to_s)
62
+ end
63
+
64
+ end
65
+
66
+ # Ensures that the attribute can be set to the given values.
67
+ #
68
+ # Note: this matcher accepts at once just one attribute to test.
69
+ # Note: this matcher is also aliased as "validate_format_of".
70
+ #
71
+ # == Options
72
+ #
73
+ # * <tt>:allow_nil</tt> - when supplied, validates if it allows nil or not.
74
+ # * <tt>:allow_blank</tt> - when supplied, validates if it allows blank or not.
75
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
76
+ # Regexp, string or symbol. Default = <tt>I18n.translate('activerecord.errors.messages.invalid')</tt>
77
+ #
78
+ # == Examples
79
+ #
80
+ # should_allow_values_for :isbn, "isbn 1 2345 6789 0", "ISBN 1-2345-6789-0"
81
+ # should_not_allow_values_for :isbn, "bad 1", "bad 2"
82
+ #
83
+ # it { should allow_values_for(:isbn, "isbn 1 2345 6789 0", "ISBN 1-2345-6789-0") }
84
+ # it { should_not allow_values_for(:isbn, "bad 1", "bad 2") }
85
+ #
86
+ def allow_values_for(attribute, *args)
87
+ options = args.extract_options!
88
+ AllowValuesForMatcher.new(attribute, options.merge!(:in => args)).spec(self)
89
+ end
90
+ alias :validate_format_of :allow_values_for
91
+
92
+ end
93
+ end
94
+ end