active_mocker 1.5.2 → 1.6

Sign up to get free protection for your applications and to get access to all the features.
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)