active_mocker 1.5.2 → 1.6

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/README.md +111 -120
  4. data/lib/active_mocker.rb +1 -1
  5. data/lib/active_mocker/active_record.rb +2 -2
  6. data/lib/active_mocker/active_record/relationships.rb +89 -79
  7. data/lib/active_mocker/active_record/scope.rb +16 -6
  8. data/lib/active_mocker/active_record/unknown_class_method.rb +7 -3
  9. data/lib/active_mocker/active_record/unknown_module.rb +19 -14
  10. data/lib/active_mocker/db_to_ruby_type.rb +1 -0
  11. data/lib/active_mocker/field.rb +1 -1
  12. data/lib/{file_reader.rb → active_mocker/file_reader.rb} +5 -0
  13. data/lib/active_mocker/generate.rb +6 -1
  14. data/lib/active_mocker/loaded_mocks.rb +43 -27
  15. data/lib/active_mocker/logger.rb +1 -0
  16. data/lib/active_mocker/mock/base.rb +153 -42
  17. data/lib/active_mocker/mock/collection.rb +6 -1
  18. data/lib/active_mocker/mock/do_nothing_active_record_methods.rb +4 -0
  19. data/lib/active_mocker/mock/exceptions.rb +14 -1
  20. data/lib/active_mocker/mock/has_many.rb +7 -0
  21. data/lib/active_mocker/mock/hash_process.rb +1 -0
  22. data/lib/active_mocker/mock/next_id.rb +1 -0
  23. data/lib/active_mocker/mock/queries.rb +216 -23
  24. data/lib/active_mocker/mock/relation.rb +21 -0
  25. data/lib/active_mocker/mock_template.erb +12 -2
  26. data/lib/active_mocker/model_reader.rb +1 -1
  27. data/lib/active_mocker/model_schema.rb +2 -1
  28. data/lib/active_mocker/public_methods.rb +24 -11
  29. data/lib/active_mocker/reparameterize.rb +1 -0
  30. data/lib/active_mocker/rspec_helper.rb +2 -0
  31. data/lib/active_mocker/schema_reader.rb +1 -0
  32. data/lib/active_mocker/string_reader.rb +14 -0
  33. data/lib/active_mocker/table.rb +1 -1
  34. data/lib/active_mocker/version.rb +1 -1
  35. metadata +4 -4
  36. data/lib/string_reader.rb +0 -9
@@ -16,7 +16,7 @@ module Mock
16
16
  end
17
17
 
18
18
  extend ::Forwardable
19
- def_delegators :@collection, :take, :push, :clear, :first, :last, :concat, :replace, :distinct, :uniq, :count, :size, :length, :empty?, :any?, :include?, :delete
19
+ def_delegators :@collection, :take, :push, :clear, :first, :last, :concat, :replace, :distinct, :uniq, :count, :size, :length, :empty?, :any?, :many?, :include?, :delete
20
20
  alias distinct uniq
21
21
 
22
22
  def select(&block)
@@ -51,6 +51,11 @@ module Mock
51
51
  @collection == val
52
52
  end
53
53
 
54
+ # Returns true if relation is blank.
55
+ def blank?
56
+ to_a.blank?
57
+ end
58
+
54
59
  protected
55
60
 
56
61
  attr_accessor :collection
@@ -52,6 +52,10 @@ module DoNothingActiveRecordMethods
52
52
  false
53
53
  end
54
54
 
55
+ def reload
56
+ self
57
+ end
58
+
55
59
  end
56
60
  end
57
61
  end
@@ -12,7 +12,17 @@ module Mock
12
12
  class FileTypeMismatchError < StandardError
13
13
  end
14
14
 
15
- class RejectedParams < Exception
15
+ # Raised when unknown attributes are supplied via mass assignment.
16
+ class UnknownAttributeError < NoMethodError
17
+
18
+ attr_reader :record, :attribute
19
+
20
+ def initialize(record, attribute)
21
+ @record = record
22
+ @attribute = attribute.to_s
23
+ super("unknown attribute: #{attribute}")
24
+ end
25
+
16
26
  end
17
27
 
18
28
  class Unimplemented < Exception
@@ -21,5 +31,8 @@ module Mock
21
31
  class IdNotNumber < Exception
22
32
  end
23
33
 
34
+ class Error < Exception
35
+ end
36
+
24
37
  end
25
38
  end
@@ -16,6 +16,13 @@ module Mock
16
16
  @foreign_id = foreign_id
17
17
  self.class.include "#{relation_class.name}::Scopes".constantize
18
18
  super(collection)
19
+ set_foreign_key if ActiveMocker::Mock.config.experimental
20
+ end
21
+
22
+ def set_foreign_key
23
+ collection.each do |item|
24
+ item.send(:write_attribute, foreign_key, foreign_id) if item.respond_to?("#{foreign_key}=")
25
+ end
19
26
  end
20
27
 
21
28
  private
@@ -1,5 +1,6 @@
1
1
  module ActiveMocker
2
2
  module Mock
3
+ # @api private
3
4
  class HashProcess
4
5
 
5
6
  attr_accessor :hash, :processor
@@ -1,5 +1,6 @@
1
1
  module ActiveMocker
2
2
  module Mock
3
+ # @api private
3
4
  class NextId
4
5
 
5
6
  def initialize(records)
@@ -9,9 +9,9 @@ module Mock
9
9
  @record = record
10
10
  end
11
11
 
12
- def is_of(options={})
13
- options.all? do |col, match|
14
- next match.any? { |m| @record.send(col) == m } if match.class == Array
12
+ def is_of(conditions={})
13
+ conditions.all? do |col, match|
14
+ next match.any? { |m| @record.send(col) == m } if match.is_a? Enumerable
15
15
  @record.send(col) == match
16
16
  end
17
17
  end
@@ -25,58 +25,231 @@ module Mock
25
25
  @parent_class = parent_class
26
26
  end
27
27
 
28
- def not(options={})
28
+ def not(conditions={})
29
29
  @parent_class.call(@collection.reject do |record|
30
- Find.new(record).is_of(options)
30
+ Find.new(record).is_of(conditions)
31
31
  end)
32
32
  end
33
33
  end
34
34
 
35
- def delete_all(options=nil)
36
- if options.nil?
35
+ # Deletes the records matching +conditions+ by instantiating each
36
+ # record and calling its +delete+ method.
37
+ #
38
+ # ==== Parameters
39
+ #
40
+ # * +conditions+ - A string, array, or hash that specifies which records
41
+ # to destroy. If omitted, all records are destroyed.
42
+ #
43
+ # ==== Examples
44
+ #
45
+ # PersonMock.destroy_all(status: "inactive")
46
+ # PersonMock.where(age: 0..18).destroy_all
47
+ #
48
+ # If a limit scope is supplied, +delete_all+ raises an ActiveMocker error:
49
+ #
50
+ # Post.limit(100).delete_all
51
+ # # => ActiveMocker::Mock::Error: delete_all doesn't support limit scope
52
+ def delete_all(conditions=nil)
53
+ raise ActiveMocker::Mock::Error.new("delete_all doesn't support limit scope") if from_limit?
54
+ if conditions.nil?
37
55
  to_a.map(&:delete)
38
56
  return to_a.clear
39
57
  end
40
- where(options).map { |r| r.delete }.count
58
+ where(conditions).map { |r| r.delete }.count
41
59
  end
42
60
 
43
- def destroy_all
44
- delete_all
45
- end
61
+ alias_method :destroy_all, :delete_all
46
62
 
47
- def where(options=nil)
48
- return WhereNotChain.new(all, method(:new_relation)) if options.nil?
63
+ # Returns a new relation, which is the result of filtering the current relation
64
+ # according to the conditions in the arguments.
65
+ #
66
+ # === hash
67
+ #
68
+ # #where will accept a hash condition, in which the keys are fields and the values
69
+ # are values to be searched for.
70
+ #
71
+ # Fields can be symbols or strings. Values can be single values, arrays, or ranges.
72
+ #
73
+ # User.where({ name: "Joe", email: "joe@example.com" })
74
+ #
75
+ # User.where({ name: ["Alice", "Bob"]})
76
+ #
77
+ # User.where({ created_at: (Time.now.midnight - 1.day)..Time.now.midnight })
78
+ #
79
+ # In the case of a belongs_to relationship, an association key can be used
80
+ # to specify the model if an ActiveRecord object is used as the value.
81
+ #
82
+ # author = Author.find(1)
83
+ #
84
+ # # The following queries will be equivalent:
85
+ # Post.where(author: author)
86
+ # Post.where(author_id: author)
87
+ #
88
+ # This also works with polymorphic belongs_to relationships:
89
+ #
90
+ # treasure = Treasure.create(name: 'gold coins')
91
+ # treasure.price_estimates << PriceEstimate.create(price: 125)
92
+ #
93
+ # # The following queries will be equivalent:
94
+ # PriceEstimate.where(estimate_of: treasure)
95
+ # PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: treasure)
96
+ #
97
+ # === no argument
98
+ #
99
+ # If no argument is passed, #where returns a new instance of WhereChain, that
100
+ # can be chained with #not to return a new relation that negates the where clause.
101
+ #
102
+ # User.where.not(name: "Jon")
103
+ #
104
+ # See WhereChain for more details on #not.
105
+ def where(conditions=nil)
106
+ return WhereNotChain.new(all, method(:new_relation)) if conditions.nil?
49
107
  new_relation(to_a.select do |record|
50
- Find.new(record).is_of(options)
108
+ Find.new(record).is_of(conditions)
51
109
  end)
52
110
  end
53
111
 
112
+ # Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
113
+ # If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key
114
+ # is an integer, find by id coerces its arguments using +to_i+.
115
+ #
116
+ # Person.find(1) # returns the object for ID = 1
117
+ # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
118
+ # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
119
+ # Person.find([1]) # returns an array for the object with ID = 1
120
+ #
121
+ # <tt>ActiveRecord::RecordNotFound</tt> will be raised if one or more ids are not found.
54
122
  def find(ids)
55
123
  results = [*ids].map do |id|
56
- where(id: id).first
124
+ find_by!(id: id)
57
125
  end
58
126
  return new_relation(results) if ids.class == Array
59
127
  results.first
60
128
  end
61
129
 
62
- def update_all(options)
63
- all.each { |i| i.update(options) }
130
+ # Updates all records with details given if they match a set of conditions supplied, limits and order can
131
+ # also be supplied.
132
+ #
133
+ # ==== Parameters
134
+ #
135
+ # * +updates+ - A string, array, or hash.
136
+ #
137
+ # ==== Examples
138
+ #
139
+ # # Update all customers with the given attributes
140
+ # Customer.update_all wants_email: true
141
+ #
142
+ # # Update all books with 'Rails' in their title
143
+ # BookMock.where(title: 'Rails').update_all(author: 'David')
144
+ #
145
+ # # Update all books that match conditions, but limit it to 5 ordered by date
146
+ # BookMock.where(title: 'Rails').order(:created_at).limit(5).update_all(author: 'David')
147
+ def update_all(conditions)
148
+ all.each { |i| i.update(conditions) }
149
+ end
150
+
151
+ # Updates an object (or multiple objects) and saves it.
152
+ #
153
+ # ==== Parameters
154
+ #
155
+ # * +id+ - This should be the id or an array of ids to be updated.
156
+ # * +attributes+ - This should be a hash of attributes or an array of hashes.
157
+ #
158
+ # ==== Examples
159
+ #
160
+ # # Updates one record
161
+ # Person.update(15, user_name: 'Samuel', group: 'expert')
162
+ #
163
+ # # Updates multiple records
164
+ # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } }
165
+ # Person.update(people.keys, people.values)
166
+ def update(id, attributes)
167
+ if id.is_a?(Array)
168
+ id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) }
169
+ else
170
+ object = find(id)
171
+ object.update(attributes)
172
+ object
173
+ end
64
174
  end
65
175
 
66
- def find_by(options = {})
67
- send(:where, options).first
176
+ # Finds the first record matching the specified conditions. There
177
+ # is no implied ordering so if order matters, you should specify it
178
+ # yourself.
179
+ #
180
+ # If no record is found, returns <tt>nil</tt>.
181
+ #
182
+ # Post.find_by name: 'Spartacus', rating: 4
183
+ def find_by(conditions = {})
184
+ send(:where, conditions).first
68
185
  end
69
186
 
70
- def find_by!(options={})
71
- result = find_by(options)
72
- raise RecordNotFound if result.blank?
187
+ # Like <tt>find_by</tt>, except that if no record is found, raises
188
+ # an <tt>ActiveRecord::RecordNotFound</tt> error.
189
+ def find_by!(conditions={})
190
+ result = find_by(conditions)
191
+ raise RecordNotFound if result.nil?
73
192
  result
74
193
  end
75
194
 
195
+ # Finds the first record with the given attributes, or creates a record
196
+ # with the attributes if one is not found:
197
+ #
198
+ # # Find the first user named "Penélope" or create a new one.
199
+ # UserMock.find_or_create_by(first_name: 'Penélope')
200
+ # # => #<User id: 1, first_name: "Penélope", last_name: nil>
201
+ #
202
+ # # Find the first user named "Penélope" or create a new one.
203
+ # # We already have one so the existing record will be returned.
204
+ # UserMock.find_or_create_by(first_name: 'Penélope')
205
+ # # => #<User id: 1, first_name: "Penélope", last_name: nil>
206
+ #
207
+ # This method accepts a block, which is passed down to +create+. The last example
208
+ # above can be alternatively written this way:
209
+ #
210
+ # # Find the first user named "Scarlett" or create a new one with a
211
+ # # different last name.
212
+ # User.find_or_create_by(first_name: 'Scarlett') do |user|
213
+ # user.last_name = 'Johansson'
214
+ # end
215
+ # # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson">
216
+ #
217
+ def find_or_create_by(attributes, &block)
218
+ find_by(attributes) || create(attributes, &block)
219
+ end
220
+
221
+ alias_method :find_or_create_by!, :find_or_create_by
222
+
223
+ # Like <tt>find_or_create_by</tt>, but calls <tt>new</tt> instead of <tt>create</tt>.
224
+ def find_or_initialize_by(attributes, &block)
225
+ find_by(attributes) || new(attributes, &block)
226
+ end
227
+
228
+ # Count the records.
229
+ #
230
+ # PersonMock.count
231
+ # # => the total count of all people
232
+ #
233
+ # PersonMock.count(:age)
234
+ # # => returns the total count of all people whose age is present in database
235
+ def count(column_name = nil)
236
+ return all.size if column_name.nil?
237
+ where.not(column_name => nil).size
238
+ end
239
+
240
+ # Specifies a limit for the number of records to retrieve.
241
+ #
242
+ # User.limit(10)
76
243
  def limit(num)
77
- new_relation(all.take(num))
244
+ relation = new_relation(all.take(num))
245
+ relation.send(:set_from_limit)
246
+ relation
78
247
  end
79
248
 
249
+ # Calculates the sum of values on a given column. The value is returned
250
+ # with the same data type of the column, 0 if there's no row.
251
+ #
252
+ # Person.sum(:age) # => 4562
80
253
  def sum(key)
81
254
  values = values_by_key(key)
82
255
  values.inject(0) do |sum, n|
@@ -84,24 +257,44 @@ module Mock
84
257
  end
85
258
  end
86
259
 
260
+ # Calculates the average value on a given column. Returns +nil+ if there's
261
+ # no row.
262
+ #
263
+ # PersonMock.average(:age) # => 35.8
87
264
  def average(key)
88
265
  values = values_by_key(key)
89
266
  total = values.inject { |sum, n| sum + n }
90
267
  BigDecimal.new(total) / BigDecimal.new(values.count)
91
268
  end
92
269
 
270
+ # Calculates the minimum value on a given column. The value is returned
271
+ # with the same data type of the column, or +nil+ if there's no row.
272
+ #
273
+ # Person.minimum(:age) # => 7
93
274
  def minimum(key)
94
275
  values_by_key(key).min_by { |i| i }
95
276
  end
96
277
 
278
+ # Calculates the maximum value on a given column. The value is returned
279
+ # with the same data type of the column, or +nil+ if there's no row.
280
+ #
281
+ # Person.maximum(:age) # => 93
97
282
  def maximum(key)
98
283
  values_by_key(key).max_by { |i| i }
99
284
  end
100
285
 
286
+ # Allows to specify an order attribute:
287
+ #
288
+ # User.order('name')
289
+ #
290
+ # User.order(:name)
101
291
  def order(key)
102
292
  new_relation(all.sort_by { |item| item.send(key) })
103
293
  end
104
294
 
295
+ # Reverse the existing order clause on the relation.
296
+ #
297
+ # User.order('name').reverse_order
105
298
  def reverse_order
106
299
  new_relation(to_a.reverse)
107
300
  end
@@ -4,6 +4,27 @@ module Mock
4
4
 
5
5
  include Queries
6
6
 
7
+ def initialize(collection=[])
8
+ super
9
+ @from_limit = false
10
+ end
11
+
12
+ def inspect
13
+ entries = to_a.take(11).map!(&:inspect)
14
+ entries[10] = '...' if entries.size == 11
15
+ "#<#{self.class.name} [#{entries.join(', ')}]>"
16
+ end
17
+
18
+ def from_limit?
19
+ @from_limit
20
+ end
21
+
22
+ private
23
+
24
+ def set_from_limit
25
+ @from_limit = true
26
+ end
27
+
7
28
  end
8
29
  end
9
30
  end
@@ -30,6 +30,8 @@ class <%= class_name + @mock_append_name %> < ActiveMocker::Mock::Base
30
30
  '<%= class_name %>'
31
31
  end
32
32
 
33
+ private :mocked_class
34
+
33
35
  def attribute_names
34
36
  @attribute_names ||= <%= attribute_names %>
35
37
  end
@@ -52,7 +54,7 @@ class <%= class_name + @mock_append_name %> < ActiveMocker::Mock::Base
52
54
  <% association = belongs_to_foreign_key(meth.name) -%>
53
55
  write_attribute(:<%= meth.name %>, val)
54
56
  <% if association -%>
55
- association = classes('<%= association.class_name %>').try(:find, <%= meth.name %>)
57
+ association = classes('<%= association.class_name %>').try(:find_by, id: <%= meth.name %>)
56
58
  write_association(:<%= association.name %>,association) unless association.nil?
57
59
  <% end -%>
58
60
  end
@@ -70,6 +72,10 @@ class <%= class_name + @mock_append_name %> < ActiveMocker::Mock::Base
70
72
  def <%= meth.name %>=(val)
71
73
  @associations[:<%= meth.name %>] = val
72
74
  write_attribute(:<%= meth.foreign_key %>, val.id) if val.respond_to?(:persisted?) && val.persisted?
75
+ if ActiveMocker::Mock.config.experimental
76
+ val.<%= class_name.tableize %> << self if val.respond_to?(:<%= class_name.tableize %>)
77
+ end
78
+ val
73
79
  end
74
80
 
75
81
  def build_<%= meth.name %>(attributes={}, &block)
@@ -92,11 +98,15 @@ class <%= class_name + @mock_append_name %> < ActiveMocker::Mock::Base
92
98
  <%= '# has_one' unless has_one.empty? -%>
93
99
  <% has_one.each do |meth| %>
94
100
  def <%= meth.name %>
95
- @associations['<%= meth.name %>']
101
+ read_association('<%= meth.name %>')
96
102
  end
97
103
 
98
104
  def <%= meth.name %>=(val)
99
105
  @associations['<%= meth.name %>'] = val
106
+ if ActiveMocker::Mock.config.experimental
107
+ <%= meth.name %>.send(:write_association, <%= class_name.tableize.singularize %>, self) if val.respond_to?(:<%= class_name.tableize.singularize %>=)
108
+ end
109
+ <%= meth.name %>
100
110
  end
101
111
 
102
112
  def build_<%= meth.name %>(attributes={}, &block)