mongo_mapper 0.6.8 → 0.6.9

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