remarkable_activerecord 3.1.13 → 4.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -13,9 +13,9 @@ Remarkable?
13
13
  :through, :source, :source_type, :class_name, :foreign_key, :dependent,
14
14
  :join_table, :uniq, :readonly, :validate, :autosave, :counter_cache, :polymorphic
15
15
 
16
- Plus SQL options:
16
+ Plus Arel scopes:
17
17
 
18
- :select, :conditions, :include, :group, :having, :order, :limit, :offset
18
+ :select, :where, :include, :group, :having, :order, :limit, :offset
19
19
 
20
20
  Besides in Remarkable 3.0 matchers became much smarter. Whenever :join_table
21
21
  or :through is given as option, it checks if the given table exists. Whenever
@@ -81,7 +81,7 @@ a few things:
81
81
  2. Include the matchers. Remarkable Rails gem is the responsable to add
82
82
  ActiveRecord matchers to rspec. If you are not using it, you have to do:
83
83
 
84
- Remarkable.include_matchers!(Remarkable::ActiveRecord, Spec::Example::ExampleGroup)
84
+ Remarkable.include_matchers!(Remarkable::ActiveRecord, Rspec::Core::ExampleGroup)
85
85
 
86
86
  This will make ActiveRecord matchers available in all rspec example groups.
87
87
 
@@ -12,8 +12,6 @@ end
12
12
  # Load Remarkable ActiveRecord files
13
13
  dir = File.dirname(__FILE__)
14
14
  require File.join(dir, 'remarkable_activerecord', 'base')
15
- require File.join(dir, 'remarkable_activerecord', 'describe')
16
- require File.join(dir, 'remarkable_activerecord', 'human_names')
17
15
 
18
16
  # Add locale
19
17
  Remarkable.add_locale File.join(dir, '..', 'locale', 'en.yml')
@@ -27,4 +25,4 @@ end
27
25
  # The responsable for this is RemarkableRails. If you are using ActiveRecord
28
26
  # without Rails, put the line below in your spec_helper to include ActiveRecord
29
27
  # matchers into rspec globally.
30
- # Remarkable.include_matchers!(Remarkable::ActiveRecord, Spec::Example::ExampleGroup)
28
+ # Remarkable.include_matchers!(Remarkable::ActiveRecord, Rspec::Example::ExampleGroup)
@@ -1,252 +1,6 @@
1
1
  module Remarkable
2
2
  module ActiveRecord
3
- class Base < Remarkable::Base
4
- I18N_COLLECTION = [ :attributes, :associations ]
5
-
6
- # Provides a way to send options to all ActiveRecord 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?(:human_name)
26
- subject_class.human_name(: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_record.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_record.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.on(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.on(attribute).blank?
143
-
144
- error_message_to_expect = error_message_from_model(model, attribute, error_message_to_expect)
145
- assert_contains(model.errors.on(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
- # ActiveRecord.
154
- #
155
- # This allows a couple things from the user side:
156
- #
157
- # 1. Specify 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 ActiveRecord 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 ::ActiveRecord.const_defined?(:Error)
185
- ::ActiveRecord::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
- ::ActiveRecord::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
-
3
+ class Base < Remarkable::ActiveModel::Base
250
4
  end
251
5
  end
252
6
  end
@@ -5,8 +5,7 @@ module Remarkable
5
5
  arguments
6
6
  assertions :options_match?
7
7
 
8
- optionals :conditions, :include, :joins, :limit, :offset, :order, :select,
9
- :readonly, :group, :having, :from, :lock
8
+ optionals :where, :having, :select, :group, :order, :limit, :offset, :joins, :includes, :lock, :readonly, :from
10
9
 
11
10
  protected
12
11
 
@@ -6,8 +6,9 @@ module Remarkable
6
6
  assertions :is_scope?, :options_match?
7
7
 
8
8
  optionals :with, :splat => true
9
- optionals :conditions, :include, :joins, :limit, :offset, :order, :select,
10
- :readonly, :group, :having, :from, :lock
9
+
10
+ # Chained scopes taken from: http://m.onkey.org/2010/1/22/active-record-query-interface
11
+ optionals :where, :having, :select, :group, :order, :limit, :offset, :joins, :includes, :lock, :readonly, :from
11
12
 
12
13
  protected
13
14
 
@@ -19,66 +20,81 @@ module Remarkable
19
20
  subject_class.send(@scope_name)
20
21
  end
21
22
 
22
- @scope_object.class == ::ActiveRecord::NamedScope::Scope
23
+ @scope_object.class == ::ActiveRecord::Relation && @scope_object.arel
23
24
  end
24
25
 
25
26
  def options_match?
26
- @options.empty? || @scope_object.proxy_options == @options.except(:with)
27
+ @options.empty? || @scope_object.arel == arel(subject_class, @options.except(:with))
27
28
  end
28
29
 
29
30
  def interpolation_options
30
- { :options => @options.except(:with).inspect,
31
- :actual => (@scope_object ? @scope_object.proxy_options.inspect : '{}')
31
+ {
32
+ :options => (subject_class.respond_to?(:scoped) ? arel(subject_class, @options.except(:with)).to_sql : '{}'),
33
+ :actual => (@scope_object ? @scope_object.arel.to_sql : '{}')
32
34
  }
33
35
  end
34
36
 
37
+ private
38
+ def arel(model, scopes = nil)
39
+ return model.scoped unless scopes
40
+ scopes.inject(model.scoped) do |chain, (cond, option)|
41
+ chain.send(cond, option)
42
+ end.arel
43
+ end
44
+
35
45
  end
36
46
 
37
- # Ensures that the model has a method named scope that returns a NamedScope
38
- # object with the supplied proxy options.
47
+ # Ensures that the model has a named scope that returns an Relation object capable
48
+ # of building into relational algebra.
39
49
  #
40
50
  # == Options
41
51
  #
42
52
  # * <tt>with</tt> - Options to be sent to the named scope
43
53
  #
44
- # All options that the named scope would pass on to find: :conditions,
45
- # :include, :joins, :limit, :offset, :order, :select, :readonly, :group,
46
- # :having, :from, :lock.
54
+ # All options that the named scope would scope with Arel:
55
+ # :where, :having, :select, :group, :order, :limit, :offset, :joins, :includes, :lock, :readonly, :from
56
+ #
57
+ # Matching is done by constructing the Arel objects and testing for equality.
47
58
  #
48
59
  # == Examples
49
60
  #
50
- # it { should have_scope(:visible, :conditions => {:visible => true}) }
51
- # it { should have_scope(:visible).conditions(:visible => true) }
61
+ # it { should have_scope(:visible, :where => {:visible => true}) }
62
+ # it { should have_scope(:visible).where(:visible => true) }
52
63
  #
53
64
  # Passes for
54
65
  #
55
- # named_scope :visible, :conditions => {:visible => true}
66
+ # scope :visible, where(:visible => true)
67
+ #
68
+ # Or for
69
+ #
70
+ # scope :visible, lambda { where(:visible => true) }
56
71
  #
57
72
  # Or for
58
73
  #
59
74
  # def self.visible
60
- # scoped(:conditions => {:visible => true})
75
+ # where(:visible => true)
61
76
  # end
62
77
  #
63
- # You can test lambdas or methods that return ActiveRecord#scoped calls:
78
+ #
79
+ # You can test lambdas or methods that return ActiveRecord#scoped calls by fixing
80
+ # a defined parameter.
64
81
  #
65
82
  # it { should have_scope(:recent, :with => 5) }
66
83
  # it { should have_scope(:recent, :with => 1) }
67
84
  #
68
85
  # Passes for
69
86
  #
70
- # named_scope :recent, lambda {|c| {:limit => c}}
87
+ # scope :recent, lambda {|c| limit(c)}
71
88
  #
72
89
  # Or for
73
90
  #
74
91
  # def self.recent(c)
75
- # scoped(:limit => c)
92
+ # limit(c)
76
93
  # end
77
94
  #
78
95
  def have_scope(*args, &block)
79
96
  HaveScopeMatcher.new(*args, &block).spec(self)
80
97
  end
81
- alias :have_named_scope :have_scope
82
98
 
83
99
  end
84
100
  end
@@ -29,7 +29,7 @@ module Remarkable
29
29
  ":builder as option or a block which returns an association." unless associated_object
30
30
 
31
31
  raise ScriptError, "The associated object #{@association} is not invalid. You can give me " <<
32
- ":builder as option or a block which returns an invalid association." if associated_object.save
32
+ ":builder as option or a block which returns an invalid association." if associated_object.valid?
33
33
 
34
34
  return true
35
35
  end
@@ -37,12 +37,9 @@ module Remarkable
37
37
  def is_valid?
38
38
  return false if @subject.valid?
39
39
 
40
- error_message_to_expect = error_message_from_model(@subject, :base, @options[:message])
40
+ error_message_to_expect = error_message_from_model(@subject, @association, @options[:message])
41
41
 
42
- # In Rails 2.1.2, the error on association returns a symbol (:invalid)
43
- # instead of the message, so we check this case here.
44
- @subject.errors.on(@association) == @options[:message] ||
45
- assert_contains(@subject.errors.on(@association), error_message_to_expect)
42
+ assert_contains(@subject.errors[@association], error_message_to_expect)
46
43
  end
47
44
  end
48
45
 
@@ -80,7 +77,7 @@ module Remarkable
80
77
  #
81
78
  # * <tt>:builder</tt> - a proc to build the association
82
79
  #
83
- # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
80
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors[:attribute]</tt>.
84
81
  # Regexp, string or symbol. Default = <tt>I18n.translate('activerecord.errors.messages.invalid')</tt>
85
82
  #
86
83
  # == Examples
@@ -207,7 +207,7 @@ module Remarkable
207
207
  # * <tt>:case_sensitive</tt> - the matcher look for an exact match.
208
208
  # * <tt>:allow_nil</tt> - when supplied, validates if it allows nil or not.
209
209
  # * <tt>:allow_blank</tt> - when supplied, validates if it allows blank or not.
210
- # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
210
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors[:attribute]</tt>.
211
211
  # Regexp, string or symbol. Default = <tt>I18n.translate('activerecord.errors.messages.taken')</tt>
212
212
  #
213
213
  # == Examples