mongo_mapper-unstable 2009.10.16 → 2009.10.31

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 (51) hide show
  1. data/.gitignore +3 -1
  2. data/README.rdoc +3 -0
  3. data/Rakefile +31 -65
  4. data/VERSION +1 -1
  5. data/lib/mongo_mapper/associations/base.rb +31 -4
  6. data/lib/mongo_mapper/associations/many_documents_as_proxy.rb +0 -2
  7. data/lib/mongo_mapper/associations/many_documents_proxy.rb +21 -15
  8. data/lib/mongo_mapper/associations/many_embedded_polymorphic_proxy.rb +2 -2
  9. data/lib/mongo_mapper/associations/many_embedded_proxy.rb +21 -36
  10. data/lib/mongo_mapper/associations/many_polymorphic_proxy.rb +1 -1
  11. data/lib/mongo_mapper/associations/proxy.rb +1 -0
  12. data/lib/mongo_mapper/associations.rb +114 -17
  13. data/lib/mongo_mapper/callbacks.rb +18 -0
  14. data/lib/mongo_mapper/document.rb +230 -95
  15. data/lib/mongo_mapper/dynamic_finder.rb +1 -1
  16. data/lib/mongo_mapper/embedded_document.rb +7 -3
  17. data/lib/mongo_mapper/finder_options.rb +88 -56
  18. data/lib/mongo_mapper/pagination.rb +2 -0
  19. data/lib/mongo_mapper/serialization.rb +2 -3
  20. data/lib/mongo_mapper/serializers/json_serializer.rb +1 -1
  21. data/lib/mongo_mapper/support.rb +9 -0
  22. data/lib/mongo_mapper/validations.rb +14 -42
  23. data/lib/mongo_mapper.rb +15 -13
  24. data/mongo_mapper.gemspec +13 -13
  25. data/specs.watchr +2 -2
  26. data/test/functional/associations/test_belongs_to_polymorphic_proxy.rb +5 -5
  27. data/test/functional/associations/test_belongs_to_proxy.rb +28 -30
  28. data/test/functional/associations/test_many_documents_as_proxy.rb +4 -4
  29. data/test/functional/associations/test_many_embedded_polymorphic_proxy.rb +27 -3
  30. data/test/functional/associations/test_many_embedded_proxy.rb +58 -38
  31. data/test/functional/associations/test_many_polymorphic_proxy.rb +49 -7
  32. data/test/functional/associations/test_many_proxy.rb +65 -15
  33. data/test/functional/test_associations.rb +3 -3
  34. data/test/functional/test_binary.rb +1 -1
  35. data/test/functional/test_callbacks.rb +1 -1
  36. data/test/functional/test_dirty.rb +3 -3
  37. data/test/functional/test_document.rb +178 -57
  38. data/test/functional/test_embedded_document.rb +1 -1
  39. data/test/functional/test_pagination.rb +18 -18
  40. data/test/functional/test_rails_compatibility.rb +1 -1
  41. data/test/functional/test_validations.rb +80 -27
  42. data/test/models.rb +93 -17
  43. data/test/support/{test_timing.rb → timing.rb} +1 -1
  44. data/test/test_helper.rb +8 -11
  45. data/test/unit/test_association_base.rb +23 -1
  46. data/test/unit/test_document.rb +29 -12
  47. data/test/unit/test_embedded_document.rb +13 -4
  48. data/test/unit/test_finder_options.rb +74 -58
  49. data/test/unit/test_mongomapper.rb +2 -2
  50. data/test/unit/test_pagination.rb +4 -0
  51. metadata +7 -7
@@ -13,12 +13,12 @@ module MongoMapper
13
13
  extend Validations::Macros
14
14
  extend ClassMethods
15
15
  extend Finders
16
-
16
+
17
17
  def self.per_page
18
18
  25
19
19
  end unless respond_to?(:per_page)
20
20
  end
21
-
21
+
22
22
  descendants << model
23
23
  end
24
24
 
@@ -32,17 +32,29 @@ module MongoMapper
32
32
  create_indexes_for(key)
33
33
  key
34
34
  end
35
-
35
+
36
36
  def ensure_index(name_or_array, options={})
37
37
  keys_to_index = if name_or_array.is_a?(Array)
38
38
  name_or_array.map { |pair| [pair[0], pair[1]] }
39
39
  else
40
40
  name_or_array
41
41
  end
42
-
42
+
43
43
  MongoMapper.ensure_index(self, keys_to_index, options)
44
44
  end
45
-
45
+
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
46
58
  def find(*args)
47
59
  options = args.extract_options!
48
60
  case args.first
@@ -55,7 +67,7 @@ module MongoMapper
55
67
  when 0
56
68
  raise DocumentNotFound, "Couldn't find without an ID"
57
69
  when 1
58
- find_one(args[0], options)
70
+ find_one!(options.merge({:_id => args[0]}))
59
71
  else
60
72
  find_some(args, options)
61
73
  end
@@ -63,68 +75,113 @@ module MongoMapper
63
75
  end
64
76
 
65
77
  def paginate(options)
66
- per_page = options.delete(:per_page) || self.per_page
78
+ per_page = options.delete(:per_page) || self.per_page
67
79
  page = options.delete(:page)
68
- total_entries = count(options[:conditions] || {})
69
- collection = Pagination::PaginationProxy.new(total_entries, page, per_page)
70
-
71
- options[:limit] = collection.limit
72
- options[:skip] = collection.skip
80
+ total_entries = count(options)
81
+ pagination = Pagination::PaginationProxy.new(total_entries, page, per_page)
73
82
 
74
- collection.subject = find_every(options)
75
- collection
83
+ options.merge!(:limit => pagination.limit, :skip => pagination.skip)
84
+ pagination.subject = find_every(options)
85
+ pagination
76
86
  end
77
87
 
88
+ # @param [Hash] options any conditions understood by
89
+ # FinderOptions.to_mongo_criteria
90
+ #
91
+ # @return the first document in the ordered collection as described by
92
+ # +options+
93
+ #
94
+ # @see FinderOptions
78
95
  def first(options={})
79
- options.merge!(:limit => 1)
80
- find_every(options)[0]
96
+ find_one(options)
81
97
  end
82
98
 
99
+ # @param [Hash] options any conditions understood by
100
+ # FinderOptions.to_mongo_criteria
101
+ # @option [String] :order this *mandatory* option describes how to
102
+ # identify the ordering of the documents in your collection. Note that
103
+ # the *last* document in this collection will be selected.
104
+ #
105
+ # @return the last document in the ordered collection as described by
106
+ # +options+
107
+ #
108
+ # @raise Exception when no <tt>:order</tt> option has been defined
83
109
  def last(options={})
84
- if options[:order].blank?
85
- raise ':order option must be provided when using last'
86
- end
87
-
88
- options.merge!(:limit => 1)
89
- options[:order] = invert_order_clause(options[:order])
90
- find_every(options)[0]
110
+ raise ':order option must be provided when using last' if options[:order].blank?
111
+ find_one(options.merge(:order => invert_order_clause(options[:order])))
91
112
  end
92
113
 
114
+ # @param [Hash] options any conditions understood by
115
+ # FinderOptions.to_mongo_criteria
116
+ #
117
+ # @return [Array] all documents in your collection that match the
118
+ # provided conditions
119
+ #
120
+ # @see FinderOptions
93
121
  def all(options={})
94
122
  find_every(options)
95
123
  end
96
124
 
97
125
  def find_by_id(id)
98
- criteria = FinderOptions.to_mongo_criteria(:_id => id)
99
- if doc = collection.find_one(criteria)
100
- new(doc)
101
- end
126
+ find_one(:_id => id)
102
127
  end
103
128
 
104
- def count(conditions={})
105
- collection.find(FinderOptions.to_mongo_criteria(conditions)).count
129
+ def count(options={})
130
+ collection.find(to_criteria(options)).count
106
131
  end
107
132
 
108
- def exists?(conditions={})
109
- !count(conditions).zero?
133
+ def exists?(options={})
134
+ !count(options).zero?
110
135
  end
111
136
 
137
+ # @overload create(doc_attributes)
138
+ # Create a single new document
139
+ # @param [Hash] doc_attributes key/value pairs to create a new
140
+ # document
141
+ #
142
+ # @overload create(docs_attributes)
143
+ # Create many new documents
144
+ # @param [Array<Hash>] provide many Hashes of key/value pairs to create
145
+ # multiple documents
146
+ #
147
+ # @example Creating a single document
148
+ # MyModel.create({ :foo => "bar" })
149
+ #
150
+ # @example Creating multiple documents
151
+ # MyModel.create([{ :foo => "bar" }, { :foo => "baz" })
152
+ #
153
+ # @return [Boolean] when a document is successfully created, +true+ will
154
+ # be returned. If a document fails to create, +false+ will be returned.
112
155
  def create(*docs)
113
156
  initialize_each(*docs) { |doc| doc.save }
114
157
  end
115
158
 
159
+ # @see Document.create
160
+ #
161
+ # @raise [DocumentNotValid] raised if a document fails to create
116
162
  def create!(*docs)
117
163
  initialize_each(*docs) { |doc| doc.save! }
118
164
  end
119
165
 
120
- # For updating single document
166
+ # @overload update(id, attributes)
167
+ # Update a single document
168
+ # @param id the ID of the document you wish to update
169
+ # @param [Hash] attributes the key to update on the document with a new
170
+ # value
171
+ #
172
+ # @overload update(ids_and_attributes)
173
+ # Update multiple documents
174
+ # @param [Hash] ids_and_attributes each key is the ID of some document
175
+ # you wish to update. The value each key points toward are those
176
+ # applied to the target document
177
+ #
178
+ # @example Updating single document
121
179
  # Person.update(1, {:foo => 'bar'})
122
180
  #
123
- # For updating multiple documents at once:
181
+ # @example Updating multiple documents at once:
124
182
  # Person.update({'1' => {:foo => 'bar'}, '2' => {:baz => 'wick'}})
125
183
  def update(*args)
126
- updating_multiple = args.length == 1
127
- if updating_multiple
184
+ if args.length == 1
128
185
  update_multiple(args[0])
129
186
  else
130
187
  id, attributes = args
@@ -132,24 +189,55 @@ module MongoMapper
132
189
  end
133
190
  end
134
191
 
192
+ # Removes ("deletes") one or many documents from the collection. Note
193
+ # that this will bypass any +destroy+ hooks defined by your class.
194
+ #
195
+ # @param [Array] ids the ID or IDs of the records you wish to delete
135
196
  def delete(*ids)
136
- criteria = FinderOptions.to_mongo_criteria(:_id => ids.flatten)
137
- collection.remove(criteria)
197
+ collection.remove(to_criteria(:_id => ids.flatten))
138
198
  end
139
199
 
140
- def delete_all(conditions={})
141
- criteria = FinderOptions.to_mongo_criteria(conditions)
142
- collection.remove(criteria)
200
+ def delete_all(options={})
201
+ collection.remove(to_criteria(options))
143
202
  end
144
203
 
204
+ # Iterates over each document found by the provided IDs and calls their
205
+ # +destroy+ method. This has the advantage of processing your document's
206
+ # +destroy+ call-backs.
207
+ #
208
+ # @overload destroy(id)
209
+ # Destroy a single document by ID
210
+ # @param id the ID of the document to destroy
211
+ #
212
+ # @overload destroy(ids)
213
+ # Destroy many documents by their IDs
214
+ # @param [Array] the IDs of each document you wish to destroy
215
+ #
216
+ # @example Destroying a single document
217
+ # Person.destroy("34")
218
+ #
219
+ # @example Destroying multiple documents
220
+ # Person.destroy("34", "45", ..., "54")
221
+ #
222
+ # # OR...
223
+ #
224
+ # Person.destroy(["34", "45", ..., "54"])
145
225
  def destroy(*ids)
146
226
  find_some(ids.flatten).each(&:destroy)
147
227
  end
148
228
 
149
- def destroy_all(conditions={})
150
- find(:all, :conditions => conditions).each(&:destroy)
229
+ def destroy_all(options={})
230
+ all(options).each(&:destroy)
151
231
  end
152
232
 
233
+ # @overload connection()
234
+ # @return [Mongo::Connection] the connection used by your document class
235
+ #
236
+ # @overload connection(mongo_connection)
237
+ # @param [Mongo::Connection] mongo_connection a new connection for your
238
+ # document class to use
239
+ # @return [Mongo::Connection] a new Mongo::Connection for yoru document
240
+ # class
153
241
  def connection(mongo_connection=nil)
154
242
  if mongo_connection.nil?
155
243
  @connection ||= MongoMapper.connection
@@ -159,42 +247,73 @@ module MongoMapper
159
247
  @connection
160
248
  end
161
249
 
162
- def database(name=nil)
163
- if name.nil?
164
- @database ||= MongoMapper.database
250
+ # Changes the database name from the default to whatever you want
251
+ #
252
+ # @param [#to_s] name the new database name to use.
253
+ def set_database_name(name)
254
+ @database_name = name
255
+ end
256
+
257
+ # Returns the database name
258
+ #
259
+ # @return [String] the database name
260
+ def database_name
261
+ @database_name
262
+ end
263
+
264
+ # Returns the database the document should use. Defaults to
265
+ # MongoMapper.database if other database is not set.
266
+ #
267
+ # @return [Mongo::DB] the mongo database instance
268
+ def database
269
+ if database_name.nil?
270
+ MongoMapper.database
165
271
  else
166
- @database = connection.db(name)
272
+ connection.db(database_name)
167
273
  end
168
- @database
169
274
  end
170
-
275
+
171
276
  # Changes the collection name from the default to whatever you want
172
- def set_collection_name(name=nil)
173
- @collection = nil
277
+ #
278
+ # @param [#to_s] name the new collection name to use.
279
+ def set_collection_name(name)
174
280
  @collection_name = name
175
281
  end
176
-
282
+
177
283
  # Returns the collection name, if not set, defaults to class name tableized
284
+ #
285
+ # @return [String] the collection name, if not set, defaults to class
286
+ # name tableized
178
287
  def collection_name
179
- @collection_name ||= self.to_s.demodulize.tableize
288
+ @collection_name ||= self.to_s.tableize.gsub(/\//, '.')
180
289
  end
181
290
 
182
- # Returns the mongo ruby driver collection object
291
+ # @return the Mongo Ruby driver +collection+ object
183
292
  def collection
184
- @collection ||= database.collection(collection_name)
293
+ database.collection(collection_name)
185
294
  end
186
295
 
296
+ # Defines a +created_at+ and +updated_at+ attribute (with a +Time+
297
+ # value) on your document. These attributes are updated by an
298
+ # injected +update_timestamps+ +before_save+ hook.
187
299
  def timestamps!
188
300
  key :created_at, Time
189
301
  key :updated_at, Time
190
-
191
302
  class_eval { before_save :update_timestamps }
192
303
  end
193
-
304
+
305
+ def single_collection_inherited?
306
+ keys.has_key?('_type') && single_collection_inherited_superclass?
307
+ end
308
+
309
+ def single_collection_inherited_superclass?
310
+ superclass.respond_to?(:keys) && superclass.keys.has_key?('_type')
311
+ end
312
+
194
313
  protected
195
314
  def method_missing(method, *args)
196
315
  finder = DynamicFinder.new(method)
197
-
316
+
198
317
  if finder.found?
199
318
  meta_def(finder.method) { |*args| dynamic_find(finder, args) }
200
319
  send(finder.method, *args)
@@ -204,50 +323,41 @@ module MongoMapper
204
323
  end
205
324
 
206
325
  private
207
- # Initializes each document and yields each initialized document
326
+ def create_indexes_for(key)
327
+ ensure_index key.name if key.options[:index]
328
+ end
329
+
208
330
  def initialize_each(*docs)
209
331
  instances = []
210
332
  docs = [{}] if docs.blank?
211
333
  docs.flatten.each do |attrs|
212
- doc = new(attrs)
334
+ doc = initialize_doc(attrs)
213
335
  yield(doc)
214
336
  instances << doc
215
337
  end
216
338
  instances.size == 1 ? instances[0] : instances
217
339
  end
218
340
 
219
- def create_indexes_for(key)
220
- ensure_index key.name if key.options[:index]
341
+ def initialize_doc(doc)
342
+ begin
343
+ klass = doc['_type'].present? ? doc['_type'].constantize : self
344
+ klass.new(doc)
345
+ rescue NameError
346
+ new(doc)
347
+ end
221
348
  end
222
-
349
+
223
350
  def find_every(options)
224
- criteria, options = FinderOptions.new(options).to_a
351
+ criteria, options = to_finder_options(options)
225
352
  collection.find(criteria, options).to_a.map do |doc|
226
- begin
227
- klass = doc['_type'].present? ? doc['_type'].constantize : self
228
- klass.new(doc)
229
- rescue NameError
230
- new(doc)
231
- end
353
+ initialize_doc(doc)
232
354
  end
233
355
  end
234
356
 
235
- def invert_order_clause(order)
236
- order.split(',').map do |order_segment|
237
- if order_segment =~ /\sasc/i
238
- order_segment.sub /\sasc/i, ' desc'
239
- elsif order_segment =~ /\sdesc/i
240
- order_segment.sub /\sdesc/i, ' asc'
241
- else
242
- "#{order_segment.strip} desc"
243
- end
244
- end.join(',')
245
- end
246
-
247
357
  def find_some(ids, options={})
248
- ids = ids.flatten.compact.uniq
249
- documents = find_every(options.deep_merge(:conditions => {'_id' => ids}))
250
-
358
+ ids = ids.flatten.compact.uniq
359
+ documents = find_every(options.merge(:_id => ids))
360
+
251
361
  if ids.size == documents.size
252
362
  documents
253
363
  else
@@ -255,14 +365,29 @@ module MongoMapper
255
365
  end
256
366
  end
257
367
 
258
- def find_one(id, options={})
259
- if doc = find_every(options.deep_merge(:conditions => {:_id => id})).first
260
- doc
261
- else
262
- raise DocumentNotFound, "Document with id of #{id} does not exist in collection named #{collection.name}"
368
+ def find_one(options={})
369
+ criteria, options = to_finder_options(options)
370
+ if doc = collection.find_one(criteria, options)
371
+ initialize_doc(doc)
263
372
  end
264
373
  end
265
374
 
375
+ def find_one!(options={})
376
+ find_one(options) || raise(DocumentNotFound, "Document match #{options.inspect} does not exist in #{collection.name} collection")
377
+ end
378
+
379
+ def invert_order_clause(order)
380
+ order.split(',').map do |order_segment|
381
+ if order_segment =~ /\sasc/i
382
+ order_segment.sub /\sasc/i, ' desc'
383
+ elsif order_segment =~ /\sdesc/i
384
+ order_segment.sub /\sdesc/i, ' asc'
385
+ else
386
+ "#{order_segment.strip} desc"
387
+ end
388
+ end.join(',')
389
+ end
390
+
266
391
  def update_single(id, attrs)
267
392
  if id.blank? || attrs.blank? || !attrs.is_a?(Hash)
268
393
  raise ArgumentError, "Updating a single document requires an id and a hash of attributes"
@@ -282,9 +407,17 @@ module MongoMapper
282
407
  docs.each_pair { |id, attrs| instances << update(id, attrs) }
283
408
  instances
284
409
  end
410
+
411
+ def to_criteria(options={})
412
+ FinderOptions.new(self, options).criteria
413
+ end
414
+
415
+ def to_finder_options(options={})
416
+ FinderOptions.new(self, options).to_a
417
+ end
285
418
  end
286
419
 
287
- module InstanceMethods
420
+ module InstanceMethods
288
421
  def collection
289
422
  self.class.collection
290
423
  end
@@ -303,12 +436,14 @@ module MongoMapper
303
436
 
304
437
  def destroy
305
438
  return false if frozen?
306
-
307
- criteria = FinderOptions.to_mongo_criteria(:_id => id)
308
- collection.remove(criteria) unless new?
439
+ self.class.delete(id) unless new?
309
440
  freeze
310
441
  end
311
442
 
443
+ def reload
444
+ self.class.find(id)
445
+ end
446
+
312
447
  private
313
448
  def create_or_update
314
449
  result = new? ? create : update
@@ -319,7 +454,7 @@ module MongoMapper
319
454
  assign_id
320
455
  save_to_collection
321
456
  end
322
-
457
+
323
458
  def assign_id
324
459
  if read_attribute(:_id).blank?
325
460
  write_attribute(:_id, Mongo::ObjectID.new.to_s)
@@ -340,7 +475,7 @@ module MongoMapper
340
475
  write_attribute('created_at', now) if new?
341
476
  write_attribute('updated_at', now)
342
477
  end
343
-
478
+
344
479
  def clear_custom_id_flag
345
480
  @using_custom_id = nil
346
481
  end
@@ -14,7 +14,7 @@ module MongoMapper
14
14
  end
15
15
 
16
16
  protected
17
- def match
17
+ def match
18
18
  case method.to_s
19
19
  when /^find_(all_by|by)_([_a-zA-Z]\w*)$/
20
20
  @finder = :all if $1 == 'all_by'
@@ -332,9 +332,13 @@ module MongoMapper
332
332
  end
333
333
 
334
334
  def read_attribute(name)
335
- value = _keys[name].get(instance_variable_get("@#{name}"))
336
- instance_variable_set "@#{name}", value if !frozen?
337
- value
335
+ if key = _keys[name]
336
+ value = key.get(instance_variable_get("@#{name}"))
337
+ instance_variable_set "@#{name}", value if !frozen?
338
+ value
339
+ else
340
+ raise KeyNotFound, "Could not find key: #{name.inspect}"
341
+ end
338
342
  end
339
343
 
340
344
  def read_attribute_before_typecast(name)