remarkable_activemodel 4.0.0.alpha1

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.
@@ -0,0 +1,147 @@
1
+ * allow_values_for checks for any message unless otherwise given [#90]
2
+
3
+ * Allow booleans to be given to validate_inclusion_of [#89]
4
+
5
+ * Allow allow_values_for and allow_mass_assignment_of matchers to be executed in
6
+ the negative form by including Remarkable::Negative module [#85]
7
+
8
+ * Ensure quick subject bypass protected attributes [#87]
9
+
10
+ * Added :token and :separator to deal with :tokenizer in validates_length_of [#77]
11
+
12
+ * Deprecated validate_format_of. It does not have the same API as the respective
13
+ ActiveModel macro, raising questions frequentely about its usage [#76]
14
+
15
+ * allow_mass_assignment_of when called without arguments checks if any mass
16
+ assignment is possible [#80]
17
+
18
+ * Add :table_name option to have_index (thanks to Lawrence Pit) [#79]
19
+
20
+ * Allow default subject attributes to be given [#74]
21
+ You can even mix with a fixture replacement tool and still use quick subjects:
22
+
23
+ describe Post
24
+ # Fixjour example
25
+ subject_attributes { valid_post_attributes }
26
+
27
+ describe :published => true do
28
+ should_validate_presence_of :published_at
29
+ end
30
+ end
31
+
32
+ * Bug fix when a symbol is given has join table to habtm association [#75]
33
+
34
+ * Association matchers now searches in the right database for tables [#73]
35
+
36
+ * validate_length_of accepts :with_kind_of to enable it to work with associations [#69]
37
+ In your Post specs now you can write:
38
+
39
+ should_validate_length_of :comments, :maximum => 10, :with_kind_of => Comment
40
+
41
+ # v3.1
42
+
43
+ * Allow validate_presence_of to work with associations [#63]
44
+
45
+ * Allow validate_uniqueness_of to work when scopes are not stringfiable values.
46
+ You can now give timestamps, datetime, date and boolean as scopes [#60]
47
+
48
+ * Allow subjects to be overwriten quickly (quick subjects):
49
+
50
+ describe Post
51
+ should_validate_presente_of :title
52
+
53
+ describe :published => true do
54
+ should_validate_presence_of :published_at
55
+ end
56
+ end
57
+
58
+ Is the same as:
59
+
60
+ describe Post
61
+ should_validate_presente_of :title
62
+
63
+ describe "when published is true" do
64
+ subject { Post.new(:published => true) }
65
+ should_validate_presence_of :published_at
66
+ end
67
+ end
68
+
69
+ And the string can be also localized using I18n [#57]
70
+
71
+ [COMPATIBILITY] validate_associated no longer accepts a block to confgure the
72
+ builder:
73
+
74
+ should_validate_associated(:tasks){ |p| p.tasks.build(:captcha => 'i_am_a_robot') }
75
+
76
+ The right way to do this is by giving an option called builder and a proc:
77
+
78
+ should_validate_associated :tasks, :builder => proc{ |p| p.tasks.build(:captcha => 'i_am_a_robot') }
79
+
80
+ * validate_uniqueness_of and accept_nested_attributes_for now use the new
81
+ interpolation option {{sentence}} [#58]
82
+
83
+ * Added accept_nested_attributes_for matcher [#39]
84
+
85
+ * Added have_default_scope matcher [#38]
86
+
87
+ * Allow :conditions, :include, :joins, :limit, :offset, :order, :select, :readonly,
88
+ :group, :having, :from, :lock as quick accessors to have_scope matcher
89
+
90
+ * Allow all kind of objects to be sent to have_scope (including datetimes, arrays,
91
+ booleans and nil) (thanks to Szymon Nowak and Nolan Eakins) [#53]
92
+
93
+ * Added support to sql options in association_matcher: select, conditions, include,
94
+ group, having, order, limit and offset, plus finder_sql and counter_sql. [#48]
95
+
96
+ * :source and :source_type are now supported by association matcher [#47]
97
+
98
+ * validate_inclusion_of became smarter since it now tests invalid values too [#36]
99
+
100
+ * Fixed three bugs in validate_uniqueness_of matcher [#42] [#40] [#37]
101
+
102
+ # v3.0
103
+
104
+ * Added more options to associations matcher. Previously it was handling just
105
+ :dependent and :through options. Now it deals with:
106
+
107
+ :through, :class_name, :foreign_key, :dependent, :join_table, :uniq,
108
+ :readonly, :validate, :autosave, :counter_cache, :polymorphic
109
+
110
+ And they are much smarter! In :join_table and :through cases, they also test if
111
+ the table exists or not. :counter_cache and :foreign_key also checks if the
112
+ column exists or not.
113
+
114
+ [COMPATIBILITY] Removed callback, have_instance_method and have_class_method
115
+ matchers. They don't lead to a good TDD since you should test they behavior
116
+ and not wether they exist or not.
117
+
118
+ [COMPATIBILITY] ActiveModel matches does not pick the instance variable from
119
+ the spec environment. So we should target only rspec versions that supports
120
+ subjects (>= 1.1.12).
121
+
122
+ Previously, when we are doing this:
123
+
124
+ describe Product
125
+ before(:each){ @product = Product.new(:tangible => true) }
126
+ should_validate_presence_of :size
127
+ end
128
+
129
+ It was validating the @product instance variable. However this might be not
130
+ clear. The right way to do that (with subjects) is:
131
+
132
+ describe Product
133
+ subject{ Product.new(:tangible => true) }
134
+ should_validate_presence_of :size
135
+ end
136
+
137
+ Is also valid to remember that previous versions of Remarkable were overriding
138
+ subject definitions on rspec. This was also fixed.
139
+
140
+ # v2.x
141
+
142
+ * Added associations, allow_mass_assignment, allow_values_for, have_column,
143
+ have_index, have_scope, have_readonly_attributes, validate_acceptance_of,
144
+ validate_associated, validate_confirmation_of, validate_exclusion_of,
145
+ validate_inclusion_of, validate_length_of, validate_numericality_of,
146
+ validate_presence_of and validate_uniqueness_of matchers.
147
+
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,74 @@
1
+ = Remarkable ActiveModel
2
+
3
+ Remarkable ActiveModel is a collection of matchers to ActiveModel. Why use
4
+ Remarkable?
5
+
6
+ * Matchers for all ActiveModel validations, with support to all options. The only
7
+ exceptions are validate_format_of (which should be invoked as allow_values_for)
8
+ and the :on option;
9
+
10
+ This means you can test your validations any model that mixes-in ActiveModel::Validations
11
+
12
+ * Tests and more tests. We have a huge tests suite ready to run and tested in
13
+ ActiveModel 3.0.0;
14
+
15
+ * Great documentation;
16
+
17
+ * I18n.
18
+
19
+ Upcoming features:
20
+ * Matchers wrapping ActiveModel::Lint for testing ActiveModel API compliance.
21
+ This means your custom models using RESTClient, for example, can play nice with
22
+ Rails.
23
+
24
+ * Matchers testing ActiveModel::Serialization
25
+ This allows you to test for compliance if you are using special serialization
26
+ methods, or require the use of Presenters in your application.
27
+
28
+
29
+ == Examples
30
+
31
+ All Remarkable macros can be accessed in two different ways. For those who prefer the Shoulda style, let’s look at some model tests:
32
+
33
+ describe Post do
34
+ should_validate_presence_of :body
35
+ should_validate_presence_of :title
36
+ end
37
+
38
+ For those who likes more the Rspec way can simply do:
39
+
40
+ describe Post do
41
+ it { should validate_presence_of(:body) }
42
+ it { should validate_presence_of(:title) }
43
+ end
44
+
45
+ == I18n
46
+
47
+ All matchers come with I18n support. If you want to translate your matchers,
48
+ grab make a copy of locale/en.yml and start to translate it.
49
+
50
+ Then add your new locale file to Remarkable:
51
+
52
+ Remarkable.add_locale 'path/to/my_locale.yml'
53
+
54
+ And then:
55
+
56
+ Remarkable.locale = :my_locale
57
+
58
+ == Using it outside Rails
59
+
60
+ If you want to use Remarkable ActiveModel outside Rails, you have to remember
61
+ a few things:
62
+
63
+ 1. Internationalization is powered by the I18n gem. If you are using it with Rails,
64
+ it will use the built in gem, otherwise you will have to install the gem by hand:
65
+
66
+ gem install i18n
67
+
68
+ 2. Include the matchers. Remarkable Rails gem is the responsable to add
69
+ ActiveModel matchers to rspec. If you are not using it, you have to do:
70
+
71
+ Remarkable.include_matchers!(Remarkable::ActiveModel, Rspec::Core::ExampleGroup)
72
+
73
+ This will make ActiveModel matchers available in all rspec example groups.
74
+
@@ -0,0 +1,28 @@
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 ActiveModel files
13
+ dir = File.dirname(__FILE__)
14
+ require File.join(dir, 'remarkable_activemodel', 'base')
15
+
16
+ # Add locale
17
+ Remarkable.add_locale File.join(dir, '..', 'locale', 'en.yml')
18
+
19
+ # Add matchers
20
+ Dir[File.join(dir, 'remarkable_activemodel', 'matchers', '*.rb')].each do |file|
21
+ require file
22
+ end
23
+
24
+ # By default, ActiveModel matchers are not included in any example group.
25
+ # The responsibility for this is RemarkableRails. If you are using ActiveModel
26
+ # without Rails, put the line below in your spec_helper to include ActiveModel
27
+ # matchers into rspec globally.
28
+ # Remarkable.include_matchers!(Remarkable::ActiveModel, Rspec::Example::ExampleGroup)
@@ -0,0 +1,252 @@
1
+ module Remarkable
2
+ module ActiveModel
3
+ class Base < Remarkable::Base
4
+ I18N_COLLECTION = [ :attributes, :associations ]
5
+
6
+ # Provides a way to send options to all ActiveModel matchers.
7
+ #
8
+ # validates_presence_of(:name).with_options(:allow_nil => false)
9
+ #
10
+ # Is equivalent to:
11
+ #
12
+ # validates_presence_of(:name, :allow_nil => false)
13
+ #
14
+ def with_options(opts={})
15
+ @options.merge!(opts)
16
+ self
17
+ end
18
+
19
+ protected
20
+
21
+ # Overwrite subject_name to provide I18n.
22
+ #
23
+ def subject_name
24
+ nil unless @subject
25
+ if subject_class.respond_to?(:model_name)
26
+ subject_class.model_name.human(:locale => Remarkable.locale)
27
+ else
28
+ subject_class.name
29
+ end
30
+ end
31
+
32
+ # Checks for the given key in @options, if it exists and it's true,
33
+ # tests that the value is bad, otherwise tests that the value is good.
34
+ #
35
+ # It accepts the key to check for, the value that is used for testing
36
+ # and an @options key where the message to search for is.
37
+ #
38
+ def assert_bad_or_good_if_key(key, value, message_key=:message) #:nodoc:
39
+ return positive? unless @options.key?(key)
40
+
41
+ if @options[key]
42
+ return bad?(value, message_key), :not => not_word
43
+ else
44
+ return good?(value, message_key), :not => ''
45
+ end
46
+ end
47
+
48
+ # Checks for the given key in @options, if it exists and it's true,
49
+ # tests that the value is good, otherwise tests that the value is bad.
50
+ #
51
+ # It accepts the key to check for, the value that is used for testing
52
+ # and an @options key where the message to search for is.
53
+ #
54
+ def assert_good_or_bad_if_key(key, value, message_key=:message) #:nodoc:
55
+ return positive? unless @options.key?(key)
56
+
57
+ if @options[key]
58
+ return good?(value, message_key), :not => ''
59
+ else
60
+ return bad?(value, message_key), :not => not_word
61
+ end
62
+ end
63
+
64
+ # Default allow_nil? validation. It accepts the message_key which is
65
+ # the key which contain the message in @options.
66
+ #
67
+ # It also gets an allow_nil message on remarkable.active_model.allow_nil
68
+ # to be used as default.
69
+ #
70
+ def allow_nil?(message_key=:message) #:nodoc:
71
+ assert_good_or_bad_if_key(:allow_nil, nil, message_key)
72
+ end
73
+
74
+ # Default allow_blank? validation. It accepts the message_key which is
75
+ # the key which contain the message in @options.
76
+ #
77
+ # It also gets an allow_blank message on remarkable.active_model.allow_blank
78
+ # to be used as default.
79
+ #
80
+ def allow_blank?(message_key=:message) #:nodoc:
81
+ assert_good_or_bad_if_key(:allow_blank, '', message_key)
82
+ end
83
+
84
+ # Shortcut for assert_good_value.
85
+ #
86
+ def good?(value, message_sym=:message) #:nodoc:
87
+ assert_good_value(@subject, @attribute, value, @options[message_sym])
88
+ end
89
+
90
+ # Shortcut for assert_bad_value.
91
+ #
92
+ def bad?(value, message_sym=:message) #:nodoc:
93
+ assert_bad_value(@subject, @attribute, value, @options[message_sym])
94
+ end
95
+
96
+ # Asserts that an Active Record model validates with the passed
97
+ # <tt>value</tt> by making sure the <tt>error_message_to_avoid</tt> is not
98
+ # contained within the list of errors for that attribute.
99
+ #
100
+ # assert_good_value(User.new, :email, "user@example.com")
101
+ # assert_good_value(User.new, :ssn, "123456789", /length/)
102
+ #
103
+ # If a class is passed as the first argument, a new object will be
104
+ # instantiated before the assertion. If an instance variable exists with
105
+ # the same name as the class (underscored), that object will be used
106
+ # instead.
107
+ #
108
+ # assert_good_value(User, :email, "user@example.com")
109
+ #
110
+ # @product = Product.new(:tangible => false)
111
+ # assert_good_value(Product, :price, "0")
112
+ #
113
+ def assert_good_value(model, attribute, value, error_message_to_avoid=//) # :nodoc:
114
+ model.send("#{attribute}=", value)
115
+
116
+ return true if model.valid?
117
+
118
+ error_message_to_avoid = error_message_from_model(model, attribute, error_message_to_avoid)
119
+ assert_does_not_contain(model.errors[attribute], error_message_to_avoid)
120
+ end
121
+
122
+ # Asserts that an Active Record model invalidates the passed
123
+ # <tt>value</tt> by making sure the <tt>error_message_to_expect</tt> is
124
+ # contained within the list of errors for that attribute.
125
+ #
126
+ # assert_bad_value(User.new, :email, "invalid")
127
+ # assert_bad_value(User.new, :ssn, "123", /length/)
128
+ #
129
+ # If a class is passed as the first argument, a new object will be
130
+ # instantiated before the assertion. If an instance variable exists with
131
+ # the same name as the class (underscored), that object will be used
132
+ # instead.
133
+ #
134
+ # assert_bad_value(User, :email, "invalid")
135
+ #
136
+ # @product = Product.new(:tangible => true)
137
+ # assert_bad_value(Product, :price, "0")
138
+ #
139
+ def assert_bad_value(model, attribute, value, error_message_to_expect=:invalid) #:nodoc:
140
+ model.send("#{attribute}=", value)
141
+
142
+ return false if model.valid? || model.errors[attribute].blank?
143
+
144
+ error_message_to_expect = error_message_from_model(model, attribute, error_message_to_expect)
145
+ assert_contains(model.errors[attribute], error_message_to_expect)
146
+ end
147
+
148
+ # Return the error message to be checked. If the message is not a Symbol
149
+ # neither a Hash, it returns the own message.
150
+ #
151
+ # But the nice thing is that when the message is a Symbol we get the error
152
+ # messsage from within the model, using already existent structure inside
153
+ # ActiveModel.
154
+ #
155
+ # This allows a couple things from the user side:
156
+ #
157
+ # 1. Rspecify symbols in their tests:
158
+ #
159
+ # should_allow_values_for(:shirt_size, 'S', 'M', 'L', :message => :inclusion)
160
+ #
161
+ # As we know, allow_values_for searches for a :invalid message. So if we
162
+ # were testing a validates_inclusion_of with allow_values_for, previously
163
+ # we had to do something like this:
164
+ #
165
+ # should_allow_values_for(:shirt_size, 'S', 'M', 'L', :message => 'not included in list')
166
+ #
167
+ # Now everything gets resumed to a Symbol.
168
+ #
169
+ # 2. Do not worry with specs if their are using I18n API properly.
170
+ #
171
+ # As we know, I18n API provides several interpolation options besides
172
+ # fallback when creating error messages. If the user changed the message,
173
+ # macros would start to pass when they shouldn't.
174
+ #
175
+ # Using the underlying mechanism inside ActiveModel makes us free from
176
+ # all thos errors.
177
+ #
178
+ # We replace {{count}} interpolation for 12345 which later is replaced
179
+ # by a regexp which contains \d+.
180
+ #
181
+ def error_message_from_model(model, attribute, message) #:nodoc:
182
+ if message.is_a? Symbol
183
+ message = if RAILS_I18N # Rails >= 2.2
184
+ if ::ActiveModel.const_defined?(:Error)
185
+ ::ActiveModel::Error.new(model, attribute, message, :count => '12345').to_s
186
+ else
187
+ model.errors.generate_message(attribute, message, :count => '12345')
188
+ end
189
+ else # Rails <= 2.1
190
+ ::ActiveModel::Errors.default_error_messages[message] % '12345'
191
+ end
192
+
193
+ if message =~ /12345/
194
+ message = Regexp.escape(message)
195
+ message.gsub!('12345', '\d+')
196
+ message = /#{message}/
197
+ end
198
+ end
199
+
200
+ message
201
+ end
202
+
203
+ # Asserts that the given collection does not contain item x. If x is a
204
+ # regular expression, ensure that none of the elements from the collection
205
+ # match x.
206
+ #
207
+ def assert_does_not_contain(collection, x) #:nodoc:
208
+ !assert_contains(collection, x)
209
+ end
210
+
211
+ # Changes how collection are interpolated to provide localized names
212
+ # whenever is possible.
213
+ #
214
+ def collection_interpolation #:nodoc:
215
+ described_class = if @subject
216
+ subject_class
217
+ elsif @spec
218
+ @spec.send(:described_class)
219
+ end
220
+
221
+ if i18n_collection? && described_class.respond_to?(:human_attribute_name)
222
+ options = {}
223
+
224
+ collection_name = self.class.matcher_arguments[:collection].to_sym
225
+ if collection = instance_variable_get("@#{collection_name}")
226
+ collection = collection.map do |attr|
227
+ described_class.human_attribute_name(attr.to_s, :locale => Remarkable.locale).downcase
228
+ end
229
+ options[collection_name] = array_to_sentence(collection)
230
+ end
231
+
232
+ object_name = self.class.matcher_arguments[:as]
233
+ if object = instance_variable_get("@#{object_name}")
234
+ object = described_class.human_attribute_name(object.to_s, :locale => Remarkable.locale).downcase
235
+ options[object_name] = object
236
+ end
237
+
238
+ options
239
+ else
240
+ super
241
+ end
242
+ end
243
+
244
+ # Returns true if the given collection should be translated.
245
+ #
246
+ def i18n_collection? #:nodoc:
247
+ RAILS_I18N && I18N_COLLECTION.include?(self.class.matcher_arguments[:collection])
248
+ end
249
+
250
+ end
251
+ end
252
+ end