remarkable_activerecord 3.1.2 → 3.1.3
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/CHANGELOG +24 -0
- data/README +3 -2
- data/lib/remarkable_activerecord/describe.rb +172 -64
- data/lib/remarkable_activerecord/matchers/allow_mass_assignment_of_matcher.rb +30 -5
- data/lib/remarkable_activerecord/matchers/allow_values_for_matcher.rb +12 -2
- data/lib/remarkable_activerecord/matchers/association_matcher.rb +26 -8
- data/lib/remarkable_activerecord/matchers/have_index_matcher.rb +18 -10
- data/lib/remarkable_activerecord/matchers/validate_uniqueness_of_matcher.rb +71 -71
- data/locale/en.yml +5 -2
- data/spec/allow_mass_assignment_of_matcher_spec.rb +26 -2
- data/spec/allow_values_for_matcher_spec.rb +7 -2
- data/spec/association_matcher_spec.rb +4 -1
- data/spec/describe_spec.rb +40 -2
- data/spec/have_index_matcher_spec.rb +24 -5
- data/spec/spec_helper.rb +1 -1
- data/spec/validate_length_of_matcher_spec.rb +24 -24
- metadata +13 -3
data/CHANGELOG
CHANGED
@@ -1,3 +1,27 @@
|
|
1
|
+
* Deprecated validate_format_of. It does not have the same API as the respective
|
2
|
+
ActiveRecord macro, raising questions frequentely about its usage. [#76]
|
3
|
+
|
4
|
+
* allow_mass_assignment_of when called without arguments checks if any mass
|
5
|
+
assignment is possible [#80]
|
6
|
+
|
7
|
+
* Add :table_name option to have_index (thanks to Lawrence Pit) [#79]
|
8
|
+
|
9
|
+
* Allow default subject attributes to be given [#74]
|
10
|
+
You can even mix with a fixture replacement tool and still use quick subjects:
|
11
|
+
|
12
|
+
describe Post
|
13
|
+
# Fixjour example
|
14
|
+
subject_attributes { valid_post_attributes }
|
15
|
+
|
16
|
+
describe :published => true do
|
17
|
+
should_validate_presence_of :published_at
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
* Bug fix when a symbol is given has join table to habtm association [#75]
|
22
|
+
|
23
|
+
* Association matchers now searches in the right database for tables [#73]
|
24
|
+
|
1
25
|
* validate_length_of accepts :with_kind_of to enable it to work with associations [#69]
|
2
26
|
In your Post specs now you can write:
|
3
27
|
|
data/README
CHANGED
@@ -3,8 +3,9 @@
|
|
3
3
|
Remarkable ActiveRecord is a collection of matchers to ActiveRecord. Why use
|
4
4
|
Remarkable?
|
5
5
|
|
6
|
-
*
|
7
|
-
|
6
|
+
* Matchers for all ActiveRecord 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;
|
8
9
|
|
9
10
|
* Matchers for all ActiveRecord associations. The only one which supports all
|
10
11
|
these options:
|
@@ -1,82 +1,190 @@
|
|
1
1
|
module Remarkable
|
2
2
|
module ActiveRecord
|
3
3
|
|
4
|
-
def self.after_include(target)
|
5
|
-
target.class_inheritable_reader :default_subject_attributes
|
6
|
-
target.
|
4
|
+
def self.after_include(target) #:nodoc:
|
5
|
+
target.class_inheritable_reader :describe_subject_attributes, :default_subject_attributes
|
6
|
+
target.send :include, Describe
|
7
7
|
end
|
8
8
|
|
9
|
-
|
9
|
+
# Overwrites describe to provide quick way to configure your subject:
|
10
|
+
#
|
11
|
+
# describe Post
|
12
|
+
# should_validate_presente_of :title
|
13
|
+
#
|
14
|
+
# describe :published => true do
|
15
|
+
# should_validate_presence_of :published_at
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# This is the same as:
|
20
|
+
#
|
21
|
+
# describe Post
|
22
|
+
# should_validate_presente_of :title
|
23
|
+
#
|
24
|
+
# describe "when published is true" do
|
25
|
+
# subject { Post.new(:published => true) }
|
26
|
+
# should_validate_presence_of :published_at
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# The string can be localized using I18n. An example yml file is:
|
31
|
+
#
|
32
|
+
# locale:
|
33
|
+
# remarkable:
|
34
|
+
# active_record:
|
35
|
+
# describe:
|
36
|
+
# each: "{{key}} is {{value}}"
|
37
|
+
# prepend: "when "
|
38
|
+
# connector: " and "
|
39
|
+
#
|
40
|
+
# You can also call subject attributes to set the default attributes for a
|
41
|
+
# subject. You can even mix with a fixture replacement tool:
|
42
|
+
#
|
43
|
+
# describe Post
|
44
|
+
# # Fixjour example
|
45
|
+
# subject_attributes { valid_post_attributes }
|
46
|
+
#
|
47
|
+
# describe :published => true do
|
48
|
+
# should_validate_presence_of :published_at
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# You can retrieve the merged result of all attributes given using the
|
53
|
+
# subject_attributes instance method:
|
54
|
+
#
|
55
|
+
# describe Post
|
56
|
+
# # Fixjour example
|
57
|
+
# subject_attributes { valid_post_attributes }
|
58
|
+
#
|
59
|
+
# describe :published => true do
|
60
|
+
# it "should have default subject attributes" do
|
61
|
+
# subject_attributes.should == { :title => 'My title', :published => true }
|
62
|
+
# end
|
63
|
+
# end
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
module Describe
|
10
67
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
# describe :published => true do
|
17
|
-
# should_validate_presence_of :published_at
|
18
|
-
# end
|
19
|
-
# end
|
20
|
-
#
|
21
|
-
# This is the same as:
|
22
|
-
#
|
23
|
-
# describe Post
|
24
|
-
# should_validate_presente_of :title
|
25
|
-
#
|
26
|
-
# describe "when published is true" do
|
27
|
-
# subject { Post.new(:published => true) }
|
28
|
-
# should_validate_presence_of :published_at
|
29
|
-
# end
|
30
|
-
# end
|
31
|
-
#
|
32
|
-
# The string can be localized using I18n. An example yml file is:
|
33
|
-
#
|
34
|
-
# locale:
|
35
|
-
# remarkable:
|
36
|
-
# active_record:
|
37
|
-
# describe:
|
38
|
-
# each: "{{key}} is {{value}}"
|
39
|
-
# prepend: "when "
|
40
|
-
# connector: " and "
|
41
|
-
#
|
42
|
-
def describe(*args, &block)
|
43
|
-
if described_class && args.first.is_a?(Hash)
|
44
|
-
attributes = args.shift
|
68
|
+
def self.included(base) #:nodoc:
|
69
|
+
base.extend ClassMethods
|
70
|
+
end
|
71
|
+
|
72
|
+
module ClassMethods
|
45
73
|
|
46
|
-
|
74
|
+
# Overwrites describe to provide quick way to configure your subject:
|
75
|
+
#
|
76
|
+
# describe Post
|
77
|
+
# should_validate_presente_of :title
|
78
|
+
#
|
79
|
+
# describe :published => true do
|
80
|
+
# should_validate_presence_of :published_at
|
81
|
+
# end
|
82
|
+
# end
|
83
|
+
#
|
84
|
+
# This is the same as:
|
85
|
+
#
|
86
|
+
# describe Post
|
87
|
+
# should_validate_presente_of :title
|
88
|
+
#
|
89
|
+
# describe "when published is true" do
|
90
|
+
# subject { Post.new(:published => true) }
|
91
|
+
# should_validate_presence_of :published_at
|
92
|
+
# end
|
93
|
+
# end
|
94
|
+
#
|
95
|
+
# The string can be localized using I18n. An example yml file is:
|
96
|
+
#
|
97
|
+
# locale:
|
98
|
+
# remarkable:
|
99
|
+
# active_record:
|
100
|
+
# describe:
|
101
|
+
# each: "{{key}} is {{value}}"
|
102
|
+
# prepend: "when "
|
103
|
+
# connector: " and "
|
104
|
+
#
|
105
|
+
# See also subject_attributes instance and class methods for more
|
106
|
+
# information.
|
107
|
+
#
|
108
|
+
def describe(*args, &block)
|
109
|
+
if described_class && args.first.is_a?(Hash)
|
110
|
+
attributes = args.shift
|
47
111
|
|
48
|
-
|
49
|
-
Remarkable.t("remarkable.active_record.describe.prepend", :default => "when ")
|
50
|
-
else
|
51
|
-
connector.lstrip
|
52
|
-
end
|
112
|
+
connector = Remarkable.t "remarkable.active_record.describe.connector", :default => " and "
|
53
113
|
|
54
|
-
|
55
|
-
|
56
|
-
translated_key = if described_class.respond_to?(:human_attribute_name)
|
57
|
-
described_class.human_attribute_name(key.to_s, :locale => Remarkable.locale)
|
114
|
+
description = if self.describe_subject_attributes.blank?
|
115
|
+
Remarkable.t("remarkable.active_record.describe.prepend", :default => "when ")
|
58
116
|
else
|
59
|
-
|
117
|
+
connector.lstrip
|
60
118
|
end
|
61
119
|
|
62
|
-
pieces
|
63
|
-
|
64
|
-
|
65
|
-
|
120
|
+
pieces = []
|
121
|
+
attributes.each do |key, value|
|
122
|
+
translated_key = if described_class.respond_to?(:human_attribute_name)
|
123
|
+
described_class.human_attribute_name(key.to_s, :locale => Remarkable.locale)
|
124
|
+
else
|
125
|
+
key.to_s.humanize
|
126
|
+
end
|
66
127
|
|
67
|
-
|
68
|
-
|
128
|
+
pieces << Remarkable.t("remarkable.active_record.describe.each",
|
129
|
+
:default => "{{key}} is {{value}}",
|
130
|
+
:key => translated_key.downcase, :value => value.inspect)
|
131
|
+
end
|
69
132
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
133
|
+
description << pieces.join(connector)
|
134
|
+
args.unshift(description)
|
135
|
+
|
136
|
+
# Creates an example group, set the subject and eval the given block.
|
137
|
+
#
|
138
|
+
example_group = super(*args) do
|
139
|
+
write_inheritable_hash(:describe_subject_attributes, attributes)
|
140
|
+
subject { self.class.described_class.new(subject_attributes) }
|
141
|
+
instance_eval(&block)
|
142
|
+
end
|
143
|
+
else
|
144
|
+
super(*args, &block)
|
76
145
|
end
|
77
|
-
|
78
|
-
|
79
|
-
|
146
|
+
end
|
147
|
+
|
148
|
+
# Sets default attributes for the subject. You can use this to set up
|
149
|
+
# your subject with valid attributes. You can even mix with a fixture
|
150
|
+
# replacement tool and still use quick subjects:
|
151
|
+
#
|
152
|
+
# describe Post
|
153
|
+
# # Fixjour example
|
154
|
+
# subject_attributes { valid_post_attributes }
|
155
|
+
#
|
156
|
+
# describe :published => true do
|
157
|
+
# should_validate_presence_of :published_at
|
158
|
+
# end
|
159
|
+
# end
|
160
|
+
#
|
161
|
+
def subject_attributes(options=nil, &block)
|
162
|
+
write_inheritable_attribute(:default_subject_attributes, options || block)
|
163
|
+
subject { self.class.described_class.new(subject_attributes) }
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
167
|
+
|
168
|
+
# Returns a hash with the subject attributes declared using the
|
169
|
+
# subject_attributes class method and the attributes given using the
|
170
|
+
# describe method.
|
171
|
+
#
|
172
|
+
# describe Post
|
173
|
+
# subject_attributes { valid_post_attributes }
|
174
|
+
#
|
175
|
+
# describe :published => true do
|
176
|
+
# it "should have default subject attributes" do
|
177
|
+
# subject_attributes.should == { :title => 'My title', :published => true }
|
178
|
+
# end
|
179
|
+
# end
|
180
|
+
# end
|
181
|
+
#
|
182
|
+
def subject_attributes
|
183
|
+
default = self.class.default_subject_attributes
|
184
|
+
default = self.instance_eval(&default) if default.is_a?(Proc)
|
185
|
+
default ||= {}
|
186
|
+
|
187
|
+
default.merge(self.class.describe_subject_attributes || {})
|
80
188
|
end
|
81
189
|
|
82
190
|
end
|
@@ -3,19 +3,44 @@ module Remarkable
|
|
3
3
|
module Matchers
|
4
4
|
class AllowMassAssignmentOfMatcher < Remarkable::ActiveRecord::Base #:nodoc:
|
5
5
|
arguments :collection => :attributes, :as => :attribute
|
6
|
-
|
6
|
+
|
7
|
+
assertion :allows?
|
7
8
|
collection_assertions :is_protected?, :is_accessible?
|
8
9
|
|
9
10
|
protected
|
11
|
+
|
12
|
+
# If no attribute is given, check if no attribute is being protected,
|
13
|
+
# otherwise it fails.
|
14
|
+
#
|
15
|
+
def allows?
|
16
|
+
!@attributes.empty? || protected_attributes.empty?
|
17
|
+
end
|
10
18
|
|
11
19
|
def is_protected?
|
12
|
-
|
13
|
-
protected.empty? || !protected.include?(@attribute.to_s)
|
20
|
+
protected_attributes.empty? || !protected_attributes.include?(@attribute.to_s)
|
14
21
|
end
|
15
22
|
|
16
23
|
def is_accessible?
|
17
|
-
|
18
|
-
|
24
|
+
accessible_attributes.empty? || accessible_attributes.include?(@attribute.to_s)
|
25
|
+
end
|
26
|
+
|
27
|
+
def interpolation_options
|
28
|
+
if @subject
|
29
|
+
array = protected_attributes.to_a
|
30
|
+
{ :protected_attributes => array.empty? ? "[]" : array_to_sentence(array) }
|
31
|
+
else
|
32
|
+
{}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def accessible_attributes
|
39
|
+
@accessible_attributes ||= subject_class.accessible_attributes || []
|
40
|
+
end
|
41
|
+
|
42
|
+
def protected_attributes
|
43
|
+
@protected_attributes ||= subject_class.protected_attributes || []
|
19
44
|
end
|
20
45
|
end
|
21
46
|
|
@@ -53,7 +53,7 @@ module Remarkable
|
|
53
53
|
options = if @in_range
|
54
54
|
{ :in => (@options[:in].first..@options[:in].last).inspect }
|
55
55
|
elsif @options[:in].is_a?(Array)
|
56
|
-
{ :in => @options[:in].map{|i| i.inspect}
|
56
|
+
{ :in => array_to_sentence(@options[:in].map{|i| i.inspect}) }
|
57
57
|
else
|
58
58
|
{ :in => @options[:in].inspect }
|
59
59
|
end
|
@@ -86,8 +86,18 @@ module Remarkable
|
|
86
86
|
def allow_values_for(attribute, *args, &block)
|
87
87
|
options = args.extract_options!
|
88
88
|
AllowValuesForMatcher.new(attribute, options.merge!(:in => args), &block).spec(self)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Deprecated. Use allow_values_for instead.
|
92
|
+
#
|
93
|
+
def validate_format_of(*args)
|
94
|
+
if caller[0] =~ /\macros.rb/
|
95
|
+
warn "[DEPRECATION] should_validate_format_of is deprecated, use should_allow_values_for instead."
|
96
|
+
else
|
97
|
+
warn "[DEPRECATION] validate_format_of is deprecated, use allow_values_for instead. Called from #{caller[0]}."
|
98
|
+
end
|
99
|
+
allow_values_for(*args)
|
89
100
|
end
|
90
|
-
alias :validate_format_of :allow_values_for
|
91
101
|
|
92
102
|
end
|
93
103
|
end
|
@@ -31,25 +31,28 @@ module Remarkable
|
|
31
31
|
return true unless @options.key?(:through)
|
32
32
|
reflection.source_reflection rescue false
|
33
33
|
end
|
34
|
-
|
34
|
+
|
35
|
+
# has_and_belongs_to_many only works if the tables are in the same
|
36
|
+
# database, so we always look for the table in the subject connection.
|
37
|
+
#
|
35
38
|
def join_table_exists?
|
36
39
|
return true unless reflection.macro == :has_and_belongs_to_many
|
37
|
-
|
40
|
+
subject_class.connection.tables.include?(reflection.options[:join_table].to_s)
|
38
41
|
end
|
39
42
|
|
40
43
|
def foreign_key_exists?
|
41
44
|
return true unless foreign_key_table
|
42
|
-
table_has_column?(foreign_key_table, reflection_foreign_key)
|
45
|
+
table_has_column?(foreign_key_table_class, foreign_key_table, reflection_foreign_key)
|
43
46
|
end
|
44
47
|
|
45
48
|
def polymorphic_exists?
|
46
49
|
return true unless @options[:polymorphic]
|
47
|
-
|
50
|
+
klass_table_has_column?(subject_class, reflection_foreign_key.sub(/_id$/, '_type'))
|
48
51
|
end
|
49
52
|
|
50
53
|
def counter_cache_exists?
|
51
54
|
return true unless @options[:counter_cache]
|
52
|
-
|
55
|
+
klass_table_has_column?(reflection.klass, reflection.counter_cache_column.to_s)
|
53
56
|
end
|
54
57
|
|
55
58
|
def options_match?
|
@@ -80,8 +83,12 @@ module Remarkable
|
|
80
83
|
reflection.primary_key_name.to_s
|
81
84
|
end
|
82
85
|
|
83
|
-
def table_has_column?(table_name, column)
|
84
|
-
|
86
|
+
def table_has_column?(klass, table_name, column)
|
87
|
+
klass.connection.columns(table_name, 'Remarkable column retrieval').any?{|c| c.name == column }
|
88
|
+
end
|
89
|
+
|
90
|
+
def klass_table_has_column?(klass, column)
|
91
|
+
table_has_column?(klass, klass.table_name, column)
|
85
92
|
end
|
86
93
|
|
87
94
|
# In through we don't check the foreign_key, because it's spread
|
@@ -104,7 +111,18 @@ module Remarkable
|
|
104
111
|
else
|
105
112
|
reflection.klass.table_name
|
106
113
|
end
|
107
|
-
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Returns the foreign key table class to use the proper connection
|
117
|
+
# when searching for the table and foreign key.
|
118
|
+
#
|
119
|
+
def foreign_key_table_class
|
120
|
+
if [:belongs_to, :has_and_belongs_to_many].include?(reflection.macro)
|
121
|
+
subject_class
|
122
|
+
else
|
123
|
+
reflection.klass
|
124
|
+
end
|
125
|
+
end
|
108
126
|
|
109
127
|
def interpolation_options
|
110
128
|
options = { :macro => Remarkable.t(@macro, :scope => matcher_i18n_scope, :default => @macro.to_s), :options => @options.inspect }
|
@@ -4,6 +4,7 @@ module Remarkable
|
|
4
4
|
class HaveIndexMatcher < Remarkable::ActiveRecord::Base #:nodoc:
|
5
5
|
arguments :collection => :columns, :as => :column
|
6
6
|
|
7
|
+
optional :table_name
|
7
8
|
optional :unique, :default => true
|
8
9
|
|
9
10
|
collection_assertions :index_exists?, :is_unique?
|
@@ -25,11 +26,17 @@ module Remarkable
|
|
25
26
|
end
|
26
27
|
|
27
28
|
def indexes
|
28
|
-
@indexes ||= ::ActiveRecord::Base.connection.indexes(
|
29
|
+
@indexes ||= ::ActiveRecord::Base.connection.indexes(current_table_name)
|
29
30
|
end
|
30
31
|
|
31
32
|
def interpolation_options
|
32
|
-
@subject ? { :table_name =>
|
33
|
+
@subject ? { :table_name => current_table_name } : {}
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def current_table_name
|
39
|
+
@options[:table_name] || subject_class.table_name
|
33
40
|
end
|
34
41
|
|
35
42
|
end
|
@@ -39,18 +46,19 @@ module Remarkable
|
|
39
46
|
# == Options
|
40
47
|
#
|
41
48
|
# * <tt>unique</tt> - when supplied, tests if the index is unique or not
|
49
|
+
# * <tt>table_name</tt> - when supplied, tests if the index is defined for the given table
|
42
50
|
#
|
43
51
|
# == Examples
|
44
52
|
#
|
45
53
|
# it { should have_index(:ssn).unique(true) }
|
46
|
-
# it { should have_index([:name, :email]).unique(true) }
|
47
|
-
#
|
48
|
-
# should_have_index :ssn, :unique => true, :limit => 9, :null => false
|
49
|
-
#
|
50
|
-
# should_have_index :ssn do |m|
|
51
|
-
# m.unique
|
52
|
-
# m.limit = 9
|
53
|
-
# m.null = false
|
54
|
+
# it { should have_index([:name, :email]).unique(true) }
|
55
|
+
#
|
56
|
+
# should_have_index :ssn, :unique => true, :limit => 9, :null => false
|
57
|
+
#
|
58
|
+
# should_have_index :ssn do |m|
|
59
|
+
# m.unique
|
60
|
+
# m.limit = 9
|
61
|
+
# m.null = false
|
54
62
|
# end
|
55
63
|
#
|
56
64
|
def have_index(*args, &block)
|
@@ -21,28 +21,28 @@ module Remarkable
|
|
21
21
|
|
22
22
|
# Tries to find an object in the database. If allow_nil and/or allow_blank
|
23
23
|
# is given, we must find a record which is not nil or not blank.
|
24
|
-
#
|
25
|
-
# We should also ensure that the object retrieved from the database
|
26
|
-
# is not the @subject.
|
24
|
+
#
|
25
|
+
# We should also ensure that the object retrieved from the database
|
26
|
+
# is not the @subject.
|
27
27
|
#
|
28
28
|
# If any of these attempts fail, an error is raised.
|
29
29
|
#
|
30
|
-
def find_first_object?
|
30
|
+
def find_first_object?
|
31
31
|
conditions, message = if @options[:allow_nil]
|
32
32
|
[ ["#{@attribute} IS NOT NULL"], " with #{@attribute} not nil" ]
|
33
33
|
elsif @options[:allow_blank]
|
34
34
|
[ ["#{@attribute} != ''"], " with #{@attribute} not blank" ]
|
35
35
|
else
|
36
36
|
[ [], "" ]
|
37
|
-
end
|
38
|
-
|
39
|
-
unless @subject.new_record?
|
40
|
-
primary_key = subject_class.primary_key
|
41
|
-
|
42
|
-
message << " which is different from the subject record (the object being validated is the same as the one in the database)"
|
43
|
-
conditions << "#{subject_class.primary_key} != '#{@subject.send(primary_key)}'"
|
44
|
-
end
|
45
|
-
|
37
|
+
end
|
38
|
+
|
39
|
+
unless @subject.new_record?
|
40
|
+
primary_key = subject_class.primary_key
|
41
|
+
|
42
|
+
message << " which is different from the subject record (the object being validated is the same as the one in the database)"
|
43
|
+
conditions << "#{subject_class.primary_key} != '#{@subject.send(primary_key)}'"
|
44
|
+
end
|
45
|
+
|
46
46
|
options = conditions.empty? ? {} : { :conditions => conditions.join(' AND ') }
|
47
47
|
|
48
48
|
return true if @existing = subject_class.find(:first, options)
|
@@ -53,9 +53,9 @@ module Remarkable
|
|
53
53
|
#
|
54
54
|
def responds_to_scope?
|
55
55
|
(@options[:scope] || []).each do |scope|
|
56
|
-
setter = :"#{scope}="
|
57
|
-
|
58
|
-
return false, :method => setter unless @subject.respond_to?(setter)
|
56
|
+
setter = :"#{scope}="
|
57
|
+
|
58
|
+
return false, :method => setter unless @subject.respond_to?(setter)
|
59
59
|
return false, :method => scope unless @existing.respond_to?(scope)
|
60
60
|
|
61
61
|
@subject.send(setter, @existing.send(scope))
|
@@ -86,11 +86,11 @@ module Remarkable
|
|
86
86
|
# Now test that the object is valid when changing the scoped attribute.
|
87
87
|
#
|
88
88
|
def valid_with_new_scope?
|
89
|
-
(@options[:scope] || []).each do |scope|
|
90
|
-
setter = :"#{scope}="
|
89
|
+
(@options[:scope] || []).each do |scope|
|
90
|
+
setter = :"#{scope}="
|
91
91
|
|
92
|
-
previous_scope_value = @subject.send(scope)
|
93
|
-
@subject.send(setter, new_value_for_scope(scope))
|
92
|
+
previous_scope_value = @subject.send(scope)
|
93
|
+
@subject.send(setter, new_value_for_scope(scope))
|
94
94
|
return false, :method => scope unless good?(@value)
|
95
95
|
|
96
96
|
@subject.send(setter, previous_scope_value)
|
@@ -106,13 +106,13 @@ module Remarkable
|
|
106
106
|
#
|
107
107
|
def allow_nil?
|
108
108
|
return true unless @options.key?(:allow_nil)
|
109
|
-
|
109
|
+
|
110
110
|
begin
|
111
111
|
@existing.update_attribute(@attribute, nil)
|
112
112
|
rescue ::ActiveRecord::StatementInvalid => e
|
113
113
|
raise ScriptError, "You declared that #{@attribute} accepts nil values in validate_uniqueness_of, " <<
|
114
|
-
"but I cannot save nil values in the database, got: #{e.message}" if @options[:allow_nil]
|
115
|
-
return true
|
114
|
+
"but I cannot save nil values in the database, got: #{e.message}" if @options[:allow_nil]
|
115
|
+
return true
|
116
116
|
end
|
117
117
|
|
118
118
|
super
|
@@ -123,53 +123,53 @@ module Remarkable
|
|
123
123
|
#
|
124
124
|
def allow_blank?
|
125
125
|
return true unless @options.key?(:allow_blank)
|
126
|
-
|
126
|
+
|
127
127
|
begin
|
128
128
|
@existing.update_attribute(@attribute, '')
|
129
129
|
rescue ::ActiveRecord::StatementInvalid => e
|
130
130
|
raise ScriptError, "You declared that #{@attribute} accepts blank values in validate_uniqueness_of, " <<
|
131
|
-
"but I cannot save blank values in the database, got: #{e.message}" if @options[:allow_blank]
|
132
|
-
return true
|
131
|
+
"but I cannot save blank values in the database, got: #{e.message}" if @options[:allow_blank]
|
132
|
+
return true
|
133
133
|
end
|
134
134
|
|
135
135
|
super
|
136
136
|
end
|
137
|
-
|
138
|
-
# Returns a value to be used as new scope. It deals with four different
|
139
|
-
# cases: date, time, boolean and stringfiable (everything that can be
|
137
|
+
|
138
|
+
# Returns a value to be used as new scope. It deals with four different
|
139
|
+
# cases: date, time, boolean and stringfiable (everything that can be
|
140
140
|
# converted to a string and the next value makes sense)
|
141
|
-
#
|
142
|
-
def new_value_for_scope(scope)
|
143
|
-
column_type = if @existing.respond_to?(:column_for_attribute)
|
144
|
-
@existing.column_for_attribute(scope)
|
145
|
-
else
|
146
|
-
nil
|
147
|
-
end
|
148
|
-
|
149
|
-
case column_type.type
|
150
|
-
when :int, :integer, :float, :decimal
|
151
|
-
new_value_for_stringfiable_scope(scope)
|
152
|
-
when :datetime, :timestamp, :time
|
153
|
-
Time.now + 10000
|
154
|
-
when :date
|
155
|
-
Date.today + 100
|
156
|
-
when :boolean
|
157
|
-
!@existing.send(scope)
|
158
|
-
else
|
159
|
-
new_value_for_stringfiable_scope(scope)
|
160
|
-
end
|
161
|
-
end
|
162
|
-
|
163
|
-
# Returns a value to be used as scope by generating a range of values
|
141
|
+
#
|
142
|
+
def new_value_for_scope(scope)
|
143
|
+
column_type = if @existing.respond_to?(:column_for_attribute)
|
144
|
+
@existing.column_for_attribute(scope)
|
145
|
+
else
|
146
|
+
nil
|
147
|
+
end
|
148
|
+
|
149
|
+
case column_type.type
|
150
|
+
when :int, :integer, :float, :decimal
|
151
|
+
new_value_for_stringfiable_scope(scope)
|
152
|
+
when :datetime, :timestamp, :time
|
153
|
+
Time.now + 10000
|
154
|
+
when :date
|
155
|
+
Date.today + 100
|
156
|
+
when :boolean
|
157
|
+
!@existing.send(scope)
|
158
|
+
else
|
159
|
+
new_value_for_stringfiable_scope(scope)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Returns a value to be used as scope by generating a range of values
|
164
164
|
# and searching for them in the database.
|
165
165
|
#
|
166
166
|
def new_value_for_stringfiable_scope(scope)
|
167
167
|
values = [(@existing.send(scope) || 999).next.to_s]
|
168
168
|
|
169
169
|
# Generate a range of values to search in the database
|
170
|
-
100.times do
|
171
|
-
values << values.last.next
|
172
|
-
end
|
170
|
+
100.times do
|
171
|
+
values << values.last.next
|
172
|
+
end
|
173
173
|
conditions = { scope => values, @attribute => @value }
|
174
174
|
|
175
175
|
# Get values from the database, get the scope attribute and map them to string.
|
@@ -190,15 +190,15 @@ module Remarkable
|
|
190
190
|
#
|
191
191
|
# Requires an existing record in the database. If you supply :allow_nil as
|
192
192
|
# option, you need to have in the database a record which is not nil in the
|
193
|
-
# given attributes. The same is required for allow_blank option.
|
194
|
-
#
|
195
|
-
# Notice that the record being validate should not be the same as in the
|
196
|
-
# database. In other words, you can't do this:
|
197
|
-
#
|
198
|
-
# subject { Post.create!(@valid_attributes) }
|
199
|
-
#
|
200
|
-
#
|
201
|
-
# But don't worry, if you eventually do that, a helpful error message
|
193
|
+
# given attributes. The same is required for allow_blank option.
|
194
|
+
#
|
195
|
+
# Notice that the record being validate should not be the same as in the
|
196
|
+
# database. In other words, you can't do this:
|
197
|
+
#
|
198
|
+
# subject { Post.create!(@valid_attributes) }
|
199
|
+
# should_validate_uniqueness_of :title
|
200
|
+
#
|
201
|
+
# But don't worry, if you eventually do that, a helpful error message
|
202
202
|
# will be raised.
|
203
203
|
#
|
204
204
|
# == Options
|
@@ -214,15 +214,15 @@ module Remarkable
|
|
214
214
|
#
|
215
215
|
# it { should validate_uniqueness_of(:keyword, :username) }
|
216
216
|
# it { should validate_uniqueness_of(:email, :scope => :name, :case_sensitive => false) }
|
217
|
-
# it { should validate_uniqueness_of(:address, :scope => [:first_name, :last_name]) }
|
218
|
-
#
|
217
|
+
# it { should validate_uniqueness_of(:address, :scope => [:first_name, :last_name]) }
|
218
|
+
#
|
219
219
|
# should_validate_uniqueness_of :keyword, :username
|
220
220
|
# should_validate_uniqueness_of :email, :scope => :name, :case_sensitive => false
|
221
|
-
# should_validate_uniqueness_of :address, :scope => [:first_name, :last_name]
|
222
|
-
#
|
223
|
-
# should_validate_uniqueness_of :email do |m|
|
224
|
-
# m.scope = name
|
225
|
-
# m.case_sensitive = false
|
221
|
+
# should_validate_uniqueness_of :address, :scope => [:first_name, :last_name]
|
222
|
+
#
|
223
|
+
# should_validate_uniqueness_of :email do |m|
|
224
|
+
# m.scope = name
|
225
|
+
# m.case_sensitive = false
|
226
226
|
# end
|
227
227
|
#
|
228
228
|
def validate_uniqueness_of(*attributes, &block)
|
data/locale/en.yml
CHANGED
@@ -41,7 +41,8 @@ en:
|
|
41
41
|
|
42
42
|
allow_mass_assignment_of:
|
43
43
|
description: "allow mass assignment of {{attributes}}"
|
44
|
-
expectations:
|
44
|
+
expectations:
|
45
|
+
allows: "{{subject_name}} to allow mass assignment ({{subject_name}} is protecting {{protected_attributes}})"
|
45
46
|
is_protected: "{{subject_name}} to allow mass assignment of {{attribute}} ({{subject_name}} is protecting {{attribute}})"
|
46
47
|
is_accessible: "{{subject_name}} to allow mass assignment of {{attribute}} ({{subject_name}} has not made {{attribute}} accessible)"
|
47
48
|
|
@@ -140,7 +141,9 @@ en:
|
|
140
141
|
optionals:
|
141
142
|
unique:
|
142
143
|
positive: "with unique values"
|
143
|
-
negative: "with non unique values"
|
144
|
+
negative: "with non unique values"
|
145
|
+
table_name:
|
146
|
+
positive: "on table {{value}}"
|
144
147
|
|
145
148
|
have_readonly_attributes:
|
146
149
|
description: "make {{attributes}} read-only"
|
@@ -20,7 +20,14 @@ describe 'allow_mass_assignment_of' do
|
|
20
20
|
@matcher = allow_mass_assignment_of(:title, :category)
|
21
21
|
@matcher.description.should == 'allow mass assignment of title and category'
|
22
22
|
end
|
23
|
-
|
23
|
+
|
24
|
+
it 'should set allows? message' do
|
25
|
+
define_and_validate(:protected => true)
|
26
|
+
@matcher = allow_mass_assignment_of
|
27
|
+
@matcher.matches?(@model)
|
28
|
+
@matcher.failure_message.should == 'Expected Product to allow mass assignment (Product is protecting category and title)'
|
29
|
+
end
|
30
|
+
|
24
31
|
it 'should set is_protected? message' do
|
25
32
|
@matcher = define_and_validate(:protected => true)
|
26
33
|
@matcher.matches?(@model)
|
@@ -40,7 +47,24 @@ describe 'allow_mass_assignment_of' do
|
|
40
47
|
it { should define_and_validate(:accessible => true) }
|
41
48
|
|
42
49
|
it { should_not define_and_validate(:protected => true) }
|
43
|
-
it { should_not define_and_validate(:accessible => [:another]) }
|
50
|
+
it { should_not define_and_validate(:accessible => [:another]) }
|
51
|
+
|
52
|
+
describe 'with no argument' do
|
53
|
+
it 'should allow mass assignment if no attribute is accessible or protected' do
|
54
|
+
define_and_validate
|
55
|
+
should allow_mass_assignment_of
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'should allow mass assignment if attributes are accessible' do
|
59
|
+
define_and_validate(:accessible => true)
|
60
|
+
should allow_mass_assignment_of
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'should not allow mass assignment if attributes are protected' do
|
64
|
+
define_and_validate(:protected => true)
|
65
|
+
should_not allow_mass_assignment_of
|
66
|
+
end
|
67
|
+
end
|
44
68
|
end
|
45
69
|
|
46
70
|
describe 'macros' do
|
@@ -49,8 +49,13 @@ describe 'allow_values_for' do
|
|
49
49
|
describe 'macros' do
|
50
50
|
before(:each){ define_and_validate(:with => /X|Y|Z/) }
|
51
51
|
|
52
|
-
should_allow_values_for :title, 'X'
|
53
|
-
should_not_allow_values_for :title, 'A'
|
52
|
+
should_allow_values_for :title, 'X'
|
53
|
+
should_not_allow_values_for :title, 'A'
|
54
|
+
|
55
|
+
describe 'deprecation' do
|
56
|
+
it { should validate_format_of(:title, 'X') }
|
57
|
+
should_not_validate_format_of :title, 'A'
|
58
|
+
end
|
54
59
|
end
|
55
60
|
end
|
56
61
|
|
@@ -273,7 +273,10 @@ describe 'association_matcher' do
|
|
273
273
|
describe 'with join table option' do
|
274
274
|
it { should define_and_validate.join_table('labels_projects') }
|
275
275
|
it { should define_and_validate(:join_table => 'my_table',
|
276
|
-
:association_table => 'my_table').join_table('my_table') }
|
276
|
+
:association_table => 'my_table').join_table('my_table') }
|
277
|
+
|
278
|
+
it { should define_and_validate(:join_table => :my_table,
|
279
|
+
:association_table => :my_table).join_table(:my_table) }
|
277
280
|
|
278
281
|
it { should_not define_and_validate.join_table('projects_labels') }
|
279
282
|
|
data/spec/describe_spec.rb
CHANGED
@@ -3,7 +3,7 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
|
3
3
|
RAILS_I18n = true
|
4
4
|
|
5
5
|
class Post
|
6
|
-
attr_accessor :
|
6
|
+
attr_accessor :published, :public, :deleted
|
7
7
|
|
8
8
|
def initialize(attributes={})
|
9
9
|
attributes.each do |key, value|
|
@@ -14,13 +14,41 @@ class Post
|
|
14
14
|
def self.human_name(*args)
|
15
15
|
"MyPost"
|
16
16
|
end
|
17
|
-
end
|
17
|
+
end
|
18
18
|
|
19
19
|
describe Post do
|
20
20
|
it "should use human name on description" do
|
21
21
|
self.class.description.should == "MyPost"
|
22
22
|
end
|
23
23
|
|
24
|
+
describe "default attributes as a hash" do
|
25
|
+
subject_attributes :deleted => true
|
26
|
+
|
27
|
+
it "should set the subject with deleted equals to true" do
|
28
|
+
subject.deleted.should be_true
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should not change the description" do
|
32
|
+
self.class.description.should == "MyPost default attributes as a hash"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "default attributes as a proc" do
|
37
|
+
subject_attributes { my_attributes }
|
38
|
+
|
39
|
+
it "should set the subject with deleted equals to true" do
|
40
|
+
subject.deleted.should be_true
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should not change the description" do
|
44
|
+
self.class.description.should == "MyPost default attributes as a proc"
|
45
|
+
end
|
46
|
+
|
47
|
+
def my_attributes
|
48
|
+
{ :deleted => true }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
24
52
|
describe :published => true do
|
25
53
|
it "should set the subject with published equals to true" do
|
26
54
|
subject.published.should be_true
|
@@ -46,6 +74,16 @@ describe Post do
|
|
46
74
|
it "should nest descriptions" do
|
47
75
|
self.class.description.should == "MyPost when published is true and public is false"
|
48
76
|
end
|
77
|
+
|
78
|
+
describe "default attributes as a hash" do
|
79
|
+
subject_attributes :deleted => true
|
80
|
+
|
81
|
+
it "should merge describe attributes with subject attributes" do
|
82
|
+
subject.published.should be_true
|
83
|
+
subject.public.should be_false
|
84
|
+
subject.deleted.should be_true
|
85
|
+
end
|
86
|
+
end
|
49
87
|
end
|
50
88
|
end
|
51
89
|
|
@@ -9,25 +9,33 @@ describe 'have_index_matcher' do
|
|
9
9
|
table.string :email, :limit => '255', :default => 'jose.valim@gmail.com'
|
10
10
|
}
|
11
11
|
|
12
|
+
create_table "users_watchers" do |t|
|
13
|
+
t.integer :user_id
|
14
|
+
t.integer :watcher_id
|
15
|
+
end
|
16
|
+
|
12
17
|
ActiveRecord::Base.connection.add_index :users, :name
|
13
18
|
ActiveRecord::Base.connection.add_index :users, :email, :unique => true
|
14
19
|
ActiveRecord::Base.connection.add_index :users, [:email, :name], :unique => true
|
20
|
+
ActiveRecord::Base.connection.add_index :users_watchers, :user_id
|
15
21
|
end
|
16
22
|
|
17
23
|
describe 'messages' do
|
18
|
-
|
19
24
|
it 'should contain a description' do
|
20
25
|
@matcher = have_index(:name)
|
21
26
|
@matcher.description.should == 'have index for column(s) name'
|
22
27
|
|
23
28
|
@matcher.unique
|
24
29
|
@matcher.description.should == 'have index for column(s) name with unique values'
|
30
|
+
|
31
|
+
@matcher.table_name("another")
|
32
|
+
@matcher.description.should == 'have index for column(s) name on table another and with unique values'
|
25
33
|
end
|
26
34
|
|
27
35
|
it 'should set index_exists? message' do
|
28
|
-
@matcher = have_index(:password)
|
36
|
+
@matcher = have_index(:password).table_name("special_users")
|
29
37
|
@matcher.matches?(@model)
|
30
|
-
@matcher.failure_message.should == 'Expected index password to exist on table
|
38
|
+
@matcher.failure_message.should == 'Expected index password to exist on table special_users'
|
31
39
|
end
|
32
40
|
|
33
41
|
it 'should set is_unique? message' do
|
@@ -41,28 +49,39 @@ describe 'have_index_matcher' do
|
|
41
49
|
it { should have_index(:name) }
|
42
50
|
it { should have_index(:email) }
|
43
51
|
it { should have_index([:email, :name]) }
|
44
|
-
it { should
|
52
|
+
it { should have_index(:name, :email) }
|
45
53
|
|
46
54
|
it { should have_index(:name).unique(false) }
|
47
55
|
it { should have_index(:email).unique }
|
56
|
+
it { should have_index(:user_id).table_name(:users_watchers) }
|
48
57
|
|
49
58
|
it { should_not have_index(:password) }
|
50
59
|
it { should_not have_index(:name).unique(true) }
|
51
60
|
it { should_not have_index(:email).unique(false) }
|
61
|
+
it { should_not have_index(:watcher_id).table_name(:users_watchers) }
|
52
62
|
end
|
53
63
|
|
54
64
|
describe 'macros' do
|
55
65
|
should_have_index :name
|
56
66
|
should_have_index :email
|
57
67
|
should_have_index [:email, :name]
|
58
|
-
|
68
|
+
should_have_index :name, :email
|
59
69
|
|
60
70
|
should_have_index :name, :unique => false
|
61
71
|
should_have_index :email, :unique => true
|
72
|
+
should_have_index :user_id, :table_name => :users_watchers
|
62
73
|
|
63
74
|
should_not_have_index :password
|
64
75
|
should_not_have_index :name, :unique => true
|
65
76
|
should_not_have_index :email, :unique => false
|
77
|
+
should_not_have_index :watcher_id, :table_name => :users_watchers
|
66
78
|
end
|
79
|
+
|
80
|
+
describe "aliases" do
|
81
|
+
should_have_indices :name
|
82
|
+
should_have_db_index :name
|
83
|
+
should_have_db_indices :name
|
84
|
+
end
|
85
|
+
|
67
86
|
end
|
68
87
|
|
data/spec/spec_helper.rb
CHANGED
@@ -143,29 +143,29 @@ describe 'validate_length_of' do
|
|
143
143
|
it { should define_and_validate(:is => 3).is(3) }
|
144
144
|
it { should_not define_and_validate(:is => 3).is(2) }
|
145
145
|
it { should_not define_and_validate(:is => 3).is(4) }
|
146
|
-
end
|
147
|
-
|
148
|
-
describe "with with kind of" do
|
149
|
-
def define_and_validate(options)
|
150
|
-
define_model :variant, :product_id => :integer
|
146
|
+
end
|
147
|
+
|
148
|
+
describe "with with kind of" do
|
149
|
+
def define_and_validate(options)
|
150
|
+
define_model :variant, :product_id => :integer
|
151
151
|
|
152
|
-
@model = define_model :product do
|
152
|
+
@model = define_model :product do
|
153
153
|
has_many :variants
|
154
|
-
validates_length_of :variants, options
|
154
|
+
validates_length_of :variants, options
|
155
155
|
end
|
156
156
|
|
157
|
-
validate_length_of(:variants)
|
158
|
-
end
|
159
|
-
|
160
|
-
it { should define_and_validate(:within => 3..6).within(3..6).with_kind_of(Variant) }
|
161
|
-
it { should_not define_and_validate(:within => 2..6).within(3..6).with_kind_of(Variant) }
|
162
|
-
it { should_not define_and_validate(:within => 3..7).within(3..6).with_kind_of(Variant) }
|
163
|
-
|
164
|
-
it "should raise association type mismatch if with_kind_of
|
165
|
-
lambda {
|
166
|
-
should_not define_and_validate(:within => 3..6).within(3..6)
|
167
|
-
}.should raise_error(ActiveRecord::AssociationTypeMismatch)
|
168
|
-
end
|
157
|
+
validate_length_of(:variants)
|
158
|
+
end
|
159
|
+
|
160
|
+
it { should define_and_validate(:within => 3..6).within(3..6).with_kind_of(Variant) }
|
161
|
+
it { should_not define_and_validate(:within => 2..6).within(3..6).with_kind_of(Variant) }
|
162
|
+
it { should_not define_and_validate(:within => 3..7).within(3..6).with_kind_of(Variant) }
|
163
|
+
|
164
|
+
it "should raise association type mismatch if with_kind_of does not match" do
|
165
|
+
lambda {
|
166
|
+
should_not define_and_validate(:within => 3..6).within(3..6).with_kind_of(Product)
|
167
|
+
}.should raise_error(ActiveRecord::AssociationTypeMismatch)
|
168
|
+
end
|
169
169
|
end
|
170
170
|
|
171
171
|
# Those are macros to test optionals which accept only boolean values
|
@@ -178,16 +178,16 @@ describe 'validate_length_of' do
|
|
178
178
|
before(:each) { define_and_validate }
|
179
179
|
|
180
180
|
should_validate_length_of :size, :in => 3..5
|
181
|
-
should_validate_length_of :size, :within => 3..5
|
181
|
+
should_validate_length_of :size, :within => 3..5
|
182
182
|
|
183
183
|
should_not_validate_length_of :size, :within => 2..5
|
184
184
|
should_not_validate_length_of :size, :within => 4..5
|
185
185
|
should_not_validate_length_of :size, :within => 3..4
|
186
186
|
should_not_validate_length_of :size, :within => 3..6
|
187
|
-
should_not_validate_length_of :category, :in => 3..5
|
188
|
-
|
189
|
-
should_validate_length_of :size do |m|
|
190
|
-
m.in = 3..5
|
187
|
+
should_not_validate_length_of :category, :in => 3..5
|
188
|
+
|
189
|
+
should_validate_length_of :size do |m|
|
190
|
+
m.in = 3..5
|
191
191
|
end
|
192
192
|
end
|
193
193
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: remarkable_activerecord
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.1.
|
4
|
+
version: 3.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Carlos Brando
|
@@ -11,9 +11,19 @@ autorequire:
|
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
13
|
|
14
|
-
date: 2009-05-
|
14
|
+
date: 2009-05-28 00:00:00 +02:00
|
15
15
|
default_executable:
|
16
16
|
dependencies:
|
17
|
+
- !ruby/object:Gem::Dependency
|
18
|
+
name: rspec
|
19
|
+
type: :runtime
|
20
|
+
version_requirement:
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 1.2.0
|
26
|
+
version:
|
17
27
|
- !ruby/object:Gem::Dependency
|
18
28
|
name: remarkable
|
19
29
|
type: :runtime
|
@@ -22,7 +32,7 @@ dependencies:
|
|
22
32
|
requirements:
|
23
33
|
- - ">="
|
24
34
|
- !ruby/object:Gem::Version
|
25
|
-
version: 3.1.
|
35
|
+
version: 3.1.3
|
26
36
|
version:
|
27
37
|
description: "Remarkable ActiveRecord: collection of matchers and macros with I18n for ActiveRecord"
|
28
38
|
email:
|