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