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 +3 -3
- data/lib/remarkable_activerecord.rb +1 -3
- data/lib/remarkable_activerecord/base.rb +1 -247
- data/lib/remarkable_activerecord/matchers/have_default_scope_matcher.rb +1 -2
- data/lib/remarkable_activerecord/matchers/have_scope_matcher.rb +35 -19
- data/lib/remarkable_activerecord/matchers/validate_associated_matcher.rb +4 -7
- data/lib/remarkable_activerecord/matchers/validate_uniqueness_of_matcher.rb +1 -1
- data/remarkable_activerecord.gemspec +44 -14
- metadata +60 -33
- data/lib/remarkable_activerecord/describe.rb +0 -198
- data/lib/remarkable_activerecord/human_names.rb +0 -37
- data/lib/remarkable_activerecord/matchers/allow_values_for_matcher.rb +0 -88
- data/lib/remarkable_activerecord/matchers/validate_acceptance_of_matcher.rb +0 -50
- data/lib/remarkable_activerecord/matchers/validate_confirmation_of_matcher.rb +0 -44
- data/lib/remarkable_activerecord/matchers/validate_exclusion_of_matcher.rb +0 -57
- data/lib/remarkable_activerecord/matchers/validate_inclusion_of_matcher.rb +0 -57
- data/lib/remarkable_activerecord/matchers/validate_length_of_matcher.rb +0 -150
- data/lib/remarkable_activerecord/matchers/validate_numericality_of_matcher.rb +0 -188
- data/lib/remarkable_activerecord/matchers/validate_presence_of_matcher.rb +0 -91
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
|
16
|
+
Plus Arel scopes:
|
17
17
|
|
18
|
-
:select, :
|
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,
|
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,
|
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 :
|
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
|
-
|
10
|
-
|
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::
|
23
|
+
@scope_object.class == ::ActiveRecord::Relation && @scope_object.arel
|
23
24
|
end
|
24
25
|
|
25
26
|
def options_match?
|
26
|
-
@options.empty? || @scope_object.
|
27
|
+
@options.empty? || @scope_object.arel == arel(subject_class, @options.except(:with))
|
27
28
|
end
|
28
29
|
|
29
30
|
def interpolation_options
|
30
|
-
{
|
31
|
-
:
|
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
|
38
|
-
#
|
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
|
45
|
-
# :
|
46
|
-
#
|
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, :
|
51
|
-
# it { should have_scope(:visible).
|
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
|
-
#
|
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
|
-
#
|
75
|
+
# where(:visible => true)
|
61
76
|
# end
|
62
77
|
#
|
63
|
-
#
|
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
|
-
#
|
87
|
+
# scope :recent, lambda {|c| limit(c)}
|
71
88
|
#
|
72
89
|
# Or for
|
73
90
|
#
|
74
91
|
# def self.recent(c)
|
75
|
-
#
|
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.
|
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,
|
40
|
+
error_message_to_expect = error_message_from_model(@subject, @association, @options[:message])
|
41
41
|
|
42
|
-
|
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
|
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
|
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
|