remarkable_activemodel 4.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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