mongo_mapper-unstable 2009.10.16 → 2009.10.31

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