mongo_mapper 0.6.8 → 0.6.9

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 (44) hide show
  1. data/README.rdoc +2 -17
  2. data/Rakefile +1 -1
  3. data/VERSION +1 -1
  4. data/lib/mongo_mapper/associations/base.rb +19 -10
  5. data/lib/mongo_mapper/associations/in_array_proxy.rb +137 -0
  6. data/lib/mongo_mapper/associations/one_proxy.rb +61 -0
  7. data/lib/mongo_mapper/associations/proxy.rb +0 -2
  8. data/lib/mongo_mapper/associations.rb +5 -3
  9. data/lib/mongo_mapper/callbacks.rb +30 -78
  10. data/lib/mongo_mapper/dirty.rb +5 -24
  11. data/lib/mongo_mapper/document.rb +117 -144
  12. data/lib/mongo_mapper/embedded_document.rb +7 -11
  13. data/lib/mongo_mapper/finder_options.rb +42 -30
  14. data/lib/mongo_mapper/mongo_mapper.rb +125 -0
  15. data/lib/mongo_mapper/pagination.rb +12 -1
  16. data/lib/mongo_mapper/rails_compatibility/embedded_document.rb +1 -0
  17. data/lib/mongo_mapper/serialization.rb +2 -2
  18. data/lib/mongo_mapper/serializers/json_serializer.rb +2 -46
  19. data/lib/mongo_mapper/support.rb +5 -2
  20. data/lib/mongo_mapper/validations.rb +1 -3
  21. data/lib/mongo_mapper.rb +8 -2
  22. data/mongo_mapper.gemspec +14 -8
  23. data/specs.watchr +3 -5
  24. data/test/functional/associations/test_belongs_to_proxy.rb +43 -0
  25. data/test/functional/associations/test_in_array_proxy.rb +309 -0
  26. data/test/functional/associations/test_many_documents_proxy.rb +103 -53
  27. data/test/functional/associations/test_many_polymorphic_proxy.rb +4 -3
  28. data/test/functional/associations/test_one_proxy.rb +131 -0
  29. data/test/functional/test_binary.rb +15 -0
  30. data/test/functional/test_document.rb +581 -631
  31. data/test/functional/test_modifiers.rb +242 -0
  32. data/test/functional/test_validations.rb +0 -17
  33. data/test/models.rb +1 -1
  34. data/test/support/timing.rb +1 -1
  35. data/test/unit/associations/test_base.rb +54 -13
  36. data/test/unit/test_document.rb +32 -0
  37. data/test/unit/test_embedded_document.rb +0 -9
  38. data/test/unit/test_finder_options.rb +36 -7
  39. data/test/unit/test_pagination.rb +6 -0
  40. data/test/unit/test_rails_compatibility.rb +4 -1
  41. data/test/unit/test_support.rb +4 -0
  42. metadata +12 -6
  43. data/lib/mongo_mapper/observing.rb +0 -50
  44. data/test/unit/test_observing.rb +0 -101
@@ -6,7 +6,6 @@ module MongoMapper
6
6
  model.class_eval do
7
7
  include EmbeddedDocument
8
8
  include InstanceMethods
9
- include Observing
10
9
  include Callbacks
11
10
  include Dirty
12
11
  include RailsCompatibility::Document
@@ -19,6 +18,9 @@ module MongoMapper
19
18
  end unless respond_to?(:per_page)
20
19
  end
21
20
 
21
+ extra_extensions.each { |extension| model.extend(extension) }
22
+ extra_inclusions.each { |inclusion| model.send(:include, inclusion) }
23
+
22
24
  descendants << model
23
25
  end
24
26
 
@@ -26,6 +28,34 @@ module MongoMapper
26
28
  @descendants ||= Set.new
27
29
  end
28
30
 
31
+ def self.append_extensions(*extensions)
32
+ extra_extensions.concat extensions
33
+
34
+ # Add the extension to existing descendants
35
+ descendants.each do |model|
36
+ extensions.each { |extension| model.extend(extension) }
37
+ end
38
+ end
39
+
40
+ # @api private
41
+ def self.extra_extensions
42
+ @extra_extensions ||= []
43
+ end
44
+
45
+ def self.append_inclusions(*inclusions)
46
+ extra_inclusions.concat inclusions
47
+
48
+ # Add the inclusion to existing descendants
49
+ descendants.each do |model|
50
+ inclusions.each { |inclusion| model.send :include, inclusion }
51
+ end
52
+ end
53
+
54
+ # @api private
55
+ def self.extra_inclusions
56
+ @extra_inclusions ||= []
57
+ end
58
+
29
59
  module ClassMethods
30
60
  def key(*args)
31
61
  key = super
@@ -43,18 +73,6 @@ module MongoMapper
43
73
  MongoMapper.ensure_index(self, keys_to_index, options)
44
74
  end
45
75
 
46
- # @overload find(:first, options)
47
- # @see Document.first
48
- #
49
- # @overload find(:last, options)
50
- # @see Document.last
51
- #
52
- # @overload find(:all, options)
53
- # @see Document.all
54
- #
55
- # @overload find(ids, options)
56
- #
57
- # @raise DocumentNotFound raised when no ID or arguments are provided
58
76
  def find!(*args)
59
77
  options = args.extract_options!
60
78
  case args.first
@@ -91,45 +109,21 @@ module MongoMapper
91
109
  pagination
92
110
  end
93
111
 
94
- # @param [Hash] options any conditions understood by
95
- # FinderOptions.to_mongo_criteria
96
- #
97
- # @return the first document in the ordered collection as described by
98
- # +options+
99
- #
100
- # @see FinderOptions
101
112
  def first(options={})
102
113
  find_one(options)
103
114
  end
104
115
 
105
- # @param [Hash] options any conditions understood by
106
- # FinderOptions.to_mongo_criteria
107
- # @option [String] :order this *mandatory* option describes how to
108
- # identify the ordering of the documents in your collection. Note that
109
- # the *last* document in this collection will be selected.
110
- #
111
- # @return the last document in the ordered collection as described by
112
- # +options+
113
- #
114
- # @raise Exception when no <tt>:order</tt> option has been defined
115
116
  def last(options={})
116
117
  raise ':order option must be provided when using last' if options[:order].blank?
117
118
  find_one(options.merge(:order => invert_order_clause(options[:order])))
118
119
  end
119
120
 
120
- # @param [Hash] options any conditions understood by
121
- # FinderOptions.to_mongo_criteria
122
- #
123
- # @return [Array] all documents in your collection that match the
124
- # provided conditions
125
- #
126
- # @see FinderOptions
127
121
  def all(options={})
128
122
  find_every(options)
129
123
  end
130
124
 
131
125
  def find_by_id(id)
132
- find_one(:_id => id)
126
+ find(id)
133
127
  end
134
128
 
135
129
  def count(options={})
@@ -140,52 +134,14 @@ module MongoMapper
140
134
  !count(options).zero?
141
135
  end
142
136
 
143
- # @overload create(doc_attributes)
144
- # Create a single new document
145
- # @param [Hash] doc_attributes key/value pairs to create a new
146
- # document
147
- #
148
- # @overload create(docs_attributes)
149
- # Create many new documents
150
- # @param [Array<Hash>] provide many Hashes of key/value pairs to create
151
- # multiple documents
152
- #
153
- # @example Creating a single document
154
- # MyModel.create({ :foo => "bar" })
155
- #
156
- # @example Creating multiple documents
157
- # MyModel.create([{ :foo => "bar" }, { :foo => "baz" })
158
- #
159
- # @return [Boolean] when a document is successfully created, +true+ will
160
- # be returned. If a document fails to create, +false+ will be returned.
161
137
  def create(*docs)
162
138
  initialize_each(*docs) { |doc| doc.save }
163
139
  end
164
140
 
165
- # @see Document.create
166
- #
167
- # @raise [DocumentNotValid] raised if a document fails to create
168
141
  def create!(*docs)
169
142
  initialize_each(*docs) { |doc| doc.save! }
170
143
  end
171
144
 
172
- # @overload update(id, attributes)
173
- # Update a single document
174
- # @param id the ID of the document you wish to update
175
- # @param [Hash] attributes the key to update on the document with a new
176
- # value
177
- #
178
- # @overload update(ids_and_attributes)
179
- # Update multiple documents
180
- # @param [Hash] ids_and_attributes each key is the ID of some document
181
- # you wish to update. The value each key points toward are those
182
- # applied to the target document
183
- #
184
- # @example Updating single document
185
- # Person.update(1, {:foo => 'bar'})
186
- #
187
- # @example Updating multiple documents at once:
188
- # Person.update({'1' => {:foo => 'bar'}, '2' => {:baz => 'wick'}})
189
145
  def update(*args)
190
146
  if args.length == 1
191
147
  update_multiple(args[0])
@@ -195,10 +151,6 @@ module MongoMapper
195
151
  end
196
152
  end
197
153
 
198
- # Removes ("deletes") one or many documents from the collection. Note
199
- # that this will bypass any +destroy+ hooks defined by your class.
200
- #
201
- # @param [Array] ids the ID or IDs of the records you wish to delete
202
154
  def delete(*ids)
203
155
  collection.remove(to_criteria(:_id => ids.flatten))
204
156
  end
@@ -207,27 +159,6 @@ module MongoMapper
207
159
  collection.remove(to_criteria(options))
208
160
  end
209
161
 
210
- # Iterates over each document found by the provided IDs and calls their
211
- # +destroy+ method. This has the advantage of processing your document's
212
- # +destroy+ call-backs.
213
- #
214
- # @overload destroy(id)
215
- # Destroy a single document by ID
216
- # @param id the ID of the document to destroy
217
- #
218
- # @overload destroy(ids)
219
- # Destroy many documents by their IDs
220
- # @param [Array] the IDs of each document you wish to destroy
221
- #
222
- # @example Destroying a single document
223
- # Person.destroy("34")
224
- #
225
- # @example Destroying multiple documents
226
- # Person.destroy("34", "45", ..., "54")
227
- #
228
- # # OR...
229
- #
230
- # Person.destroy(["34", "45", ..., "54"])
231
162
  def destroy(*ids)
232
163
  find_some(ids.flatten).each(&:destroy)
233
164
  end
@@ -235,15 +166,58 @@ module MongoMapper
235
166
  def destroy_all(options={})
236
167
  all(options).each(&:destroy)
237
168
  end
169
+
170
+ def increment(*args)
171
+ modifier_update('$inc', args)
172
+ end
173
+
174
+ def decrement(*args)
175
+ criteria, keys = criteria_and_keys_from_args(args)
176
+ values, to_decrement = keys.values, {}
177
+ keys.keys.each_with_index { |k, i| to_decrement[k] = -values[i].abs }
178
+ collection.update(criteria, {'$inc' => to_decrement}, :multi => true)
179
+ end
180
+
181
+ def set(*args)
182
+ modifier_update('$set', args)
183
+ end
184
+
185
+ def push(*args)
186
+ modifier_update('$push', args)
187
+ end
188
+
189
+ def push_all(*args)
190
+ modifier_update('$pushAll', args)
191
+ end
192
+
193
+ def push_uniq(*args)
194
+ criteria, keys = criteria_and_keys_from_args(args)
195
+ keys.each { |key, value | criteria[key] = {'$ne' => value} }
196
+ collection.update(criteria, {'$push' => keys}, :multi => true)
197
+ end
198
+
199
+ def pull(*args)
200
+ modifier_update('$pull', args)
201
+ end
202
+
203
+ def pull_all(*args)
204
+ modifier_update('$pullAll', args)
205
+ end
206
+
207
+ def modifier_update(modifier, args)
208
+ criteria, keys = criteria_and_keys_from_args(args)
209
+ modifiers = {modifier => keys}
210
+ collection.update(criteria, modifiers, :multi => true)
211
+ end
212
+ private :modifier_update
213
+
214
+ def criteria_and_keys_from_args(args)
215
+ keys = args.pop
216
+ criteria = args[0].is_a?(Hash) ? args[0] : {:id => args}
217
+ [to_criteria(criteria), keys]
218
+ end
219
+ private :criteria_and_keys_from_args
238
220
 
239
- # @overload connection()
240
- # @return [Mongo::Connection] the connection used by your document class
241
- #
242
- # @overload connection(mongo_connection)
243
- # @param [Mongo::Connection] mongo_connection a new connection for your
244
- # document class to use
245
- # @return [Mongo::Connection] a new Mongo::Connection for yoru document
246
- # class
247
221
  def connection(mongo_connection=nil)
248
222
  if mongo_connection.nil?
249
223
  @connection ||= MongoMapper.connection
@@ -253,24 +227,14 @@ module MongoMapper
253
227
  @connection
254
228
  end
255
229
 
256
- # Changes the database name from the default to whatever you want
257
- #
258
- # @param [#to_s] name the new database name to use.
259
230
  def set_database_name(name)
260
231
  @database_name = name
261
232
  end
262
233
 
263
- # Returns the database name
264
- #
265
- # @return [String] the database name
266
234
  def database_name
267
235
  @database_name
268
236
  end
269
237
 
270
- # Returns the database the document should use. Defaults to
271
- # MongoMapper.database if other database is not set.
272
- #
273
- # @return [Mongo::DB] the mongo database instance
274
238
  def database
275
239
  if database_name.nil?
276
240
  MongoMapper.database
@@ -279,35 +243,31 @@ module MongoMapper
279
243
  end
280
244
  end
281
245
 
282
- # Changes the collection name from the default to whatever you want
283
- #
284
- # @param [#to_s] name the new collection name to use.
285
246
  def set_collection_name(name)
286
247
  @collection_name = name
287
248
  end
288
249
 
289
- # Returns the collection name, if not set, defaults to class name tableized
290
- #
291
- # @return [String] the collection name, if not set, defaults to class
292
- # name tableized
293
250
  def collection_name
294
251
  @collection_name ||= self.to_s.tableize.gsub(/\//, '.')
295
252
  end
296
253
 
297
- # @return the Mongo Ruby driver +collection+ object
298
254
  def collection
299
255
  database.collection(collection_name)
300
256
  end
301
-
302
- # Defines a +created_at+ and +updated_at+ attribute (with a +Time+
303
- # value) on your document. These attributes are updated by an
304
- # injected +update_timestamps+ +before_save+ hook.
257
+
305
258
  def timestamps!
306
259
  key :created_at, Time
307
260
  key :updated_at, Time
308
261
  class_eval { before_save :update_timestamps }
309
262
  end
310
263
 
264
+ def userstamps!
265
+ key :creator_id, ObjectId
266
+ key :updater_id, ObjectId
267
+ belongs_to :creator, :class_name => 'User'
268
+ belongs_to :updater, :class_name => 'User'
269
+ end
270
+
311
271
  def single_collection_inherited?
312
272
  keys.has_key?('_type') && single_collection_inherited_superclass?
313
273
  end
@@ -406,13 +366,23 @@ module MongoMapper
406
366
  def collection
407
367
  self.class.collection
408
368
  end
369
+
370
+ def database
371
+ self.class.database
372
+ end
409
373
 
410
374
  def new?
411
375
  read_attribute('_id').blank? || using_custom_id?
412
376
  end
413
377
 
414
- def save(perform_validations=true)
415
- !perform_validations || valid? ? create_or_update : false
378
+ def save(options={})
379
+ if options === false
380
+ ActiveSupport::Deprecation.warn "save with true/false is deprecated. You should now use :validate => true/false."
381
+ options = {:validate => false}
382
+ end
383
+ options.reverse_merge!(:validate => true)
384
+ perform_validations = options.delete(:validate)
385
+ !perform_validations || valid? ? create_or_update(options) : false
416
386
  end
417
387
 
418
388
  def save!
@@ -420,9 +390,11 @@ module MongoMapper
420
390
  end
421
391
 
422
392
  def destroy
423
- return false if frozen?
424
- self.class.delete(_id) unless new?
425
- freeze
393
+ self.class.delete(id) unless new?
394
+ end
395
+
396
+ def delete
397
+ self.class.delete(id) unless new?
426
398
  end
427
399
 
428
400
  def reload
@@ -433,14 +405,14 @@ module MongoMapper
433
405
  end
434
406
 
435
407
  private
436
- def create_or_update
437
- result = new? ? create : update
408
+ def create_or_update(options={})
409
+ result = new? ? create(options) : update(options)
438
410
  result != false
439
411
  end
440
412
 
441
- def create
413
+ def create(options={})
442
414
  assign_id
443
- save_to_collection
415
+ save_to_collection(options)
444
416
  end
445
417
 
446
418
  def assign_id
@@ -449,13 +421,14 @@ module MongoMapper
449
421
  end
450
422
  end
451
423
 
452
- def update
453
- save_to_collection
424
+ def update(options={})
425
+ save_to_collection(options)
454
426
  end
455
427
 
456
- def save_to_collection
428
+ def save_to_collection(options={})
457
429
  clear_custom_id_flag
458
- collection.save(to_mongo)
430
+ safe = options.delete(:safe) || false
431
+ collection.save(to_mongo, :safe => safe)
459
432
  end
460
433
 
461
434
  def update_timestamps
@@ -281,7 +281,7 @@ module MongoMapper
281
281
  end
282
282
 
283
283
  def id
284
- read_attribute(:_id)
284
+ self[:_id]
285
285
  end
286
286
 
287
287
  def id=(value)
@@ -291,7 +291,7 @@ module MongoMapper
291
291
  @using_custom_id = true
292
292
  end
293
293
 
294
- write_attribute :_id, value
294
+ self[:_id] = value
295
295
  end
296
296
 
297
297
  def using_custom_id?
@@ -305,16 +305,12 @@ module MongoMapper
305
305
  "#<#{self.class} #{attributes_as_nice_string}>"
306
306
  end
307
307
 
308
- def save
309
- if _root_document
310
- _root_document.save
311
- end
308
+ def save(options={})
309
+ _root_document.try(:save, options)
312
310
  end
313
311
 
314
- def save!
315
- if _root_document
316
- _root_document.save!
317
- end
312
+ def save!(options={})
313
+ _root_document.try(:save!, options)
318
314
  end
319
315
 
320
316
  def update_attributes(attrs={})
@@ -355,7 +351,7 @@ module MongoMapper
355
351
  def read_attribute(name)
356
352
  if key = _keys[name]
357
353
  value = key.get(instance_variable_get("@#{name}"))
358
- instance_variable_set "@#{name}", value if !frozen?
354
+ instance_variable_set "@#{name}", value
359
355
  value
360
356
  else
361
357
  raise KeyNotFound, "Could not find key: #{name.inspect}"
@@ -1,17 +1,20 @@
1
1
  module MongoMapper
2
- # Controls the parsing and handling of options used by finders.
3
- #
4
- # == Important Note
5
- #
2
+ # = Important Note
6
3
  # This class is private to MongoMapper and should not be considered part of
7
- # MongoMapper's public API. Some documentation herein, however, may prove
8
- # useful for understanding how MongoMapper handles the parsing of finder
9
- # conditions and options.
4
+ # MongoMapper's public API.
10
5
  #
11
- # @private
12
6
  class FinderOptions
13
7
  OptionKeys = [:fields, :select, :skip, :offset, :limit, :sort, :order]
14
8
 
9
+ def self.normalized_field(field)
10
+ field.to_s == 'id' ? :_id : field
11
+ end
12
+
13
+ def self.normalized_order_direction(direction)
14
+ direction ||= 'ASC'
15
+ direction.upcase == 'ASC' ? 1 : -1
16
+ end
17
+
15
18
  def initialize(model, options)
16
19
  raise ArgumentError, "Options must be a hash" unless options.is_a?(Hash)
17
20
  options = options.symbolize_keys
@@ -30,15 +33,11 @@ module MongoMapper
30
33
 
31
34
  add_sci_scope
32
35
  end
33
-
34
- # @return [Hash] Mongo compatible criteria options
35
- #
36
- # @see FinderOptions#to_mongo_criteria
36
+
37
37
  def criteria
38
38
  to_mongo_criteria(@conditions)
39
39
  end
40
-
41
- # @return [Hash] Mongo compatible options
40
+
42
41
  def options
43
42
  fields = @options.delete(:fields) || @options.delete(:select)
44
43
  skip = @options.delete(:skip) || @options.delete(:offset) || 0
@@ -47,8 +46,7 @@ module MongoMapper
47
46
 
48
47
  {:fields => to_mongo_fields(fields), :skip => skip.to_i, :limit => limit.to_i, :sort => sort}
49
48
  end
50
-
51
- # @return [Array<Hash>] Mongo criteria and options enclosed in an Array
49
+
52
50
  def to_a
53
51
  [criteria, options]
54
52
  end
@@ -58,14 +56,14 @@ module MongoMapper
58
56
  criteria = {}
59
57
 
60
58
  conditions.each_pair do |field, value|
61
- field = normalized_field(field)
59
+ field = self.class.normalized_field(field)
62
60
 
63
61
  if @model.object_id_key?(field) && value.is_a?(String)
64
62
  value = Mongo::ObjectID.from_string(value)
65
63
  end
66
64
 
67
65
  if field.is_a?(FinderOperator)
68
- criteria.merge!(field.to_criteria(value))
66
+ criteria.update(field.to_criteria(value))
69
67
  next
70
68
  end
71
69
 
@@ -74,6 +72,8 @@ module MongoMapper
74
72
  criteria[field] = operator?(field) ? value : {'$in' => value}
75
73
  when Hash
76
74
  criteria[field] = to_mongo_criteria(value, field)
75
+ when Time
76
+ criteria[field] = value.utc
77
77
  else
78
78
  criteria[field] = value
79
79
  end
@@ -86,10 +86,6 @@ module MongoMapper
86
86
  field.to_s =~ /^\$/
87
87
  end
88
88
 
89
- def normalized_field(field)
90
- field.to_s == 'id' ? :_id : field
91
- end
92
-
93
89
  # adds _type single collection inheritance scope for models that need it
94
90
  def add_sci_scope
95
91
  if @model.single_collection_inherited?
@@ -100,23 +96,29 @@ module MongoMapper
100
96
  def to_mongo_fields(fields)
101
97
  return if fields.blank?
102
98
 
103
- if fields.is_a?(String)
104
- fields.split(',').map { |field| field.strip }
105
- else
99
+ if fields.respond_to?(:flatten, :compact)
106
100
  fields.flatten.compact
101
+ else
102
+ fields.split(',').map { |field| field.strip }
107
103
  end
108
104
  end
109
105
 
110
106
  def convert_order_to_sort(sort)
111
107
  return if sort.blank?
112
- pieces = sort.split(',')
113
- pieces.map { |s| to_mongo_sort_piece(s) }
108
+
109
+ if sort.respond_to?(:all?) && sort.all? { |s| s.respond_to?(:to_order) }
110
+ sort.map { |s| s.to_order }
111
+ elsif sort.respond_to?(:to_order)
112
+ [sort.to_order]
113
+ else
114
+ pieces = sort.split(',')
115
+ pieces.map { |s| to_mongo_sort_piece(s) }
116
+ end
114
117
  end
115
118
 
116
119
  def to_mongo_sort_piece(str)
117
120
  field, direction = str.strip.split(' ')
118
- direction ||= 'ASC'
119
- direction = direction.upcase == 'ASC' ? 1 : -1
121
+ direction = FinderOptions.normalized_order_direction(direction)
120
122
  [field, direction]
121
123
  end
122
124
  end
@@ -127,7 +129,17 @@ module MongoMapper
127
129
  end
128
130
 
129
131
  def to_criteria(value)
130
- {@field => {@operator => value}}
132
+ {FinderOptions.normalized_field(@field) => {@operator => value}}
133
+ end
134
+ end
135
+
136
+ class OrderOperator
137
+ def initialize(field, direction)
138
+ @field, @direction = field, direction
139
+ end
140
+
141
+ def to_order
142
+ [@field.to_s, FinderOptions.normalized_order_direction(@direction)]
131
143
  end
132
144
  end
133
145
  end