remarkable_activerecord 3.1.13 → 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.
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