danielharan-mongo_mapper 0.6.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. data/.gitignore +10 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +53 -0
  4. data/Rakefile +55 -0
  5. data/VERSION +1 -0
  6. data/bin/mmconsole +60 -0
  7. data/lib/mongo_mapper.rb +134 -0
  8. data/lib/mongo_mapper/associations.rb +183 -0
  9. data/lib/mongo_mapper/associations/base.rb +110 -0
  10. data/lib/mongo_mapper/associations/belongs_to_polymorphic_proxy.rb +34 -0
  11. data/lib/mongo_mapper/associations/belongs_to_proxy.rb +22 -0
  12. data/lib/mongo_mapper/associations/many_documents_as_proxy.rb +25 -0
  13. data/lib/mongo_mapper/associations/many_documents_proxy.rb +127 -0
  14. data/lib/mongo_mapper/associations/many_embedded_polymorphic_proxy.rb +33 -0
  15. data/lib/mongo_mapper/associations/many_embedded_proxy.rb +53 -0
  16. data/lib/mongo_mapper/associations/many_polymorphic_proxy.rb +11 -0
  17. data/lib/mongo_mapper/associations/many_proxy.rb +6 -0
  18. data/lib/mongo_mapper/associations/proxy.rb +80 -0
  19. data/lib/mongo_mapper/callbacks.rb +109 -0
  20. data/lib/mongo_mapper/dirty.rb +136 -0
  21. data/lib/mongo_mapper/document.rb +481 -0
  22. data/lib/mongo_mapper/dynamic_finder.rb +35 -0
  23. data/lib/mongo_mapper/embedded_document.rb +386 -0
  24. data/lib/mongo_mapper/finder_options.rb +133 -0
  25. data/lib/mongo_mapper/key.rb +36 -0
  26. data/lib/mongo_mapper/observing.rb +50 -0
  27. data/lib/mongo_mapper/pagination.rb +53 -0
  28. data/lib/mongo_mapper/rails_compatibility/document.rb +15 -0
  29. data/lib/mongo_mapper/rails_compatibility/embedded_document.rb +27 -0
  30. data/lib/mongo_mapper/serialization.rb +54 -0
  31. data/lib/mongo_mapper/serializers/json_serializer.rb +92 -0
  32. data/lib/mongo_mapper/support.rb +193 -0
  33. data/lib/mongo_mapper/validations.rb +41 -0
  34. data/mongo_mapper.gemspec +171 -0
  35. data/specs.watchr +32 -0
  36. data/test/NOTE_ON_TESTING +1 -0
  37. data/test/functional/associations/test_belongs_to_polymorphic_proxy.rb +55 -0
  38. data/test/functional/associations/test_belongs_to_proxy.rb +48 -0
  39. data/test/functional/associations/test_many_documents_as_proxy.rb +246 -0
  40. data/test/functional/associations/test_many_embedded_polymorphic_proxy.rb +156 -0
  41. data/test/functional/associations/test_many_embedded_proxy.rb +196 -0
  42. data/test/functional/associations/test_many_polymorphic_proxy.rb +339 -0
  43. data/test/functional/associations/test_many_proxy.rb +384 -0
  44. data/test/functional/test_associations.rb +44 -0
  45. data/test/functional/test_binary.rb +18 -0
  46. data/test/functional/test_callbacks.rb +85 -0
  47. data/test/functional/test_dirty.rb +159 -0
  48. data/test/functional/test_document.rb +1180 -0
  49. data/test/functional/test_embedded_document.rb +125 -0
  50. data/test/functional/test_logger.rb +20 -0
  51. data/test/functional/test_pagination.rb +95 -0
  52. data/test/functional/test_rails_compatibility.rb +25 -0
  53. data/test/functional/test_string_id_compatibility.rb +72 -0
  54. data/test/functional/test_validations.rb +369 -0
  55. data/test/models.rb +271 -0
  56. data/test/support/custom_matchers.rb +55 -0
  57. data/test/support/timing.rb +16 -0
  58. data/test/test_helper.rb +27 -0
  59. data/test/unit/serializers/test_json_serializer.rb +189 -0
  60. data/test/unit/test_association_base.rb +166 -0
  61. data/test/unit/test_document.rb +204 -0
  62. data/test/unit/test_dynamic_finder.rb +125 -0
  63. data/test/unit/test_embedded_document.rb +718 -0
  64. data/test/unit/test_finder_options.rb +296 -0
  65. data/test/unit/test_key.rb +172 -0
  66. data/test/unit/test_mongo_mapper.rb +65 -0
  67. data/test/unit/test_observing.rb +101 -0
  68. data/test/unit/test_pagination.rb +113 -0
  69. data/test/unit/test_rails_compatibility.rb +49 -0
  70. data/test/unit/test_serializations.rb +52 -0
  71. data/test/unit/test_support.rb +342 -0
  72. data/test/unit/test_time_zones.rb +40 -0
  73. data/test/unit/test_validations.rb +503 -0
  74. metadata +233 -0
@@ -0,0 +1,109 @@
1
+ module MongoMapper
2
+ # This module is mixed into the Document module to provide call-backs before
3
+ # and after the following events:
4
+ #
5
+ # * save
6
+ # * create
7
+ # * update
8
+ # * validation
9
+ # ** every validation
10
+ # ** validation when created
11
+ # ** validation when updated
12
+ # * destruction
13
+ #
14
+ # @see ActiveSupport::Callbacks
15
+ module Callbacks
16
+ def self.included(model) #:nodoc:
17
+ model.class_eval do
18
+ extend Observable
19
+ include ActiveSupport::Callbacks
20
+
21
+ callbacks = %w(
22
+ before_save
23
+ after_save
24
+ before_create
25
+ after_create
26
+ before_update
27
+ after_update
28
+ before_validation
29
+ after_validation
30
+ before_validation_on_create
31
+ after_validation_on_create
32
+ before_validation_on_update
33
+ after_validation_on_update
34
+ before_destroy
35
+ after_destroy
36
+ )
37
+
38
+ define_callbacks(*callbacks)
39
+
40
+ callbacks.each do |callback|
41
+ define_method(callback.to_sym) {}
42
+ end
43
+ end
44
+ end
45
+
46
+ def valid? #:nodoc:
47
+ return false if callback(:before_validation) == false
48
+ result = new? ? callback(:before_validation_on_create) : callback(:before_validation_on_update)
49
+ return false if false == result
50
+
51
+ result = super
52
+ callback(:after_validation)
53
+
54
+ new? ? callback(:after_validation_on_create) : callback(:after_validation_on_update)
55
+ return result
56
+ end
57
+
58
+ # Here we override the +destroy+ method to allow for the +before_destroy+
59
+ # and +after_destroy+ call-backs. Note that the +destroy+ call is aborted
60
+ # if the +before_destroy+ call-back returns +false+.
61
+ #
62
+ # @return the result of calling +destroy+ on the document
63
+ def destroy #:nodoc:
64
+ return false if callback(:before_destroy) == false
65
+ result = super
66
+ callback(:after_destroy)
67
+ result
68
+ end
69
+
70
+ private
71
+ def callback(method)
72
+ result = run_callbacks(method) { |result, object| false == result }
73
+
74
+ if result != false && respond_to?(method)
75
+ result = send(method)
76
+ end
77
+
78
+ notify(method)
79
+ return result
80
+ end
81
+
82
+ def notify(method) #:nodoc:
83
+ self.class.changed
84
+ self.class.notify_observers(method, self)
85
+ end
86
+
87
+ def create_or_update #:nodoc:
88
+ return false if callback(:before_save) == false
89
+ if result = super
90
+ callback(:after_save)
91
+ end
92
+ result
93
+ end
94
+
95
+ def create #:nodoc:
96
+ return false if callback(:before_create) == false
97
+ result = super
98
+ callback(:after_create)
99
+ result
100
+ end
101
+
102
+ def update(*args) #:nodoc:
103
+ return false if callback(:before_update) == false
104
+ result = super
105
+ callback(:after_update)
106
+ result
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,136 @@
1
+ module MongoMapper
2
+ module Dirty
3
+ DIRTY_SUFFIXES = ['_changed?', '_change', '_will_change!', '_was']
4
+
5
+ def method_missing(method, *args, &block)
6
+ if method.to_s =~ /(_changed\?|_change|_will_change!|_was)$/
7
+ method_suffix = $1
8
+ key = method.to_s.gsub(method_suffix, '')
9
+
10
+ if key_names.include?(key)
11
+ case method_suffix
12
+ when '_changed?'
13
+ key_changed?(key)
14
+ when '_change'
15
+ key_change(key)
16
+ when '_will_change!'
17
+ key_will_change!(key)
18
+ when '_was'
19
+ key_was(key)
20
+ end
21
+ else
22
+ super
23
+ end
24
+ else
25
+ super
26
+ end
27
+ end
28
+
29
+ def changed?
30
+ !changed_keys.empty?
31
+ end
32
+
33
+ # List of keys with unsaved changes.
34
+ # person.changed # => []
35
+ # person.name = 'bob'
36
+ # person.changed # => ['name']
37
+ def changed
38
+ changed_keys.keys
39
+ end
40
+
41
+ # Map of changed attrs => [original value, new value].
42
+ # person.changes # => {}
43
+ # person.name = 'bob'
44
+ # person.changes # => { 'name' => ['bill', 'bob'] }
45
+ def changes
46
+ changed.inject({}) { |h, attribute| h[attribute] = key_change(attribute); h }
47
+ end
48
+
49
+ def initialize(attrs={})
50
+ super(attrs)
51
+ changed_keys.clear unless new?
52
+ end
53
+
54
+ # Attempts to +save+ the record and clears changed keys if successful.
55
+ def save(*args)
56
+ if status = super
57
+ changed_keys.clear
58
+ end
59
+ status
60
+ end
61
+
62
+ # Attempts to <tt>save!</tt> the record and clears changed keys if successful.
63
+ def save!(*args)
64
+ status = super
65
+ changed_keys.clear
66
+ status
67
+ end
68
+
69
+ # <tt>reload</tt> the record and clears changed keys.
70
+ # def reload_with_dirty(*args) #:nodoc:
71
+ # record = reload_without_dirty(*args)
72
+ # changed_keys.clear
73
+ # record
74
+ # end
75
+
76
+ private
77
+ def clone_key_value(attribute_name)
78
+ value = send(:read_attribute, attribute_name)
79
+ value.duplicable? ? value.clone : value
80
+ rescue TypeError, NoMethodError
81
+ value
82
+ end
83
+
84
+ # Map of change <tt>attr => original value</tt>.
85
+ def changed_keys
86
+ @changed_keys ||= {}
87
+ end
88
+
89
+ # Handle <tt>*_changed?</tt> for +method_missing+.
90
+ def key_changed?(attribute)
91
+ changed_keys.include?(attribute)
92
+ end
93
+
94
+ # Handle <tt>*_change</tt> for +method_missing+.
95
+ def key_change(attribute)
96
+ [changed_keys[attribute], __send__(attribute)] if key_changed?(attribute)
97
+ end
98
+
99
+ # Handle <tt>*_was</tt> for +method_missing+.
100
+ def key_was(attribute)
101
+ key_changed?(attribute) ? changed_keys[attribute] : __send__(attribute)
102
+ end
103
+
104
+ # Handle <tt>*_will_change!</tt> for +method_missing+.
105
+ def key_will_change!(attribute)
106
+ changed_keys[attribute] = clone_key_value(attribute)
107
+ end
108
+
109
+ # Wrap write_attribute to remember original key value.
110
+ def write_attribute(attribute, value)
111
+ attribute = attribute.to_s
112
+
113
+ # The key already has an unsaved change.
114
+ if changed_keys.include?(attribute)
115
+ old = changed_keys[attribute]
116
+ changed_keys.delete(attribute) unless value_changed?(attribute, old, value)
117
+ else
118
+ old = clone_key_value(attribute)
119
+ changed_keys[attribute] = old if value_changed?(attribute, old, value)
120
+ end
121
+
122
+ # Carry on.
123
+ super(attribute, value)
124
+ end
125
+
126
+ def value_changed?(key_name, old, value)
127
+ key = _keys[key_name]
128
+
129
+ if key.number? && value.blank?
130
+ value = nil
131
+ end
132
+
133
+ old != value
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,481 @@
1
+ require 'set'
2
+
3
+ module MongoMapper
4
+ module Document
5
+ def self.included(model)
6
+ model.class_eval do
7
+ include EmbeddedDocument
8
+ include InstanceMethods
9
+ include Observing
10
+ include Callbacks
11
+ include Dirty
12
+ include RailsCompatibility::Document
13
+ extend Validations::Macros
14
+ extend ClassMethods
15
+ extend Finders
16
+
17
+ def self.per_page
18
+ 25
19
+ end unless respond_to?(:per_page)
20
+ end
21
+
22
+ descendants << model
23
+ end
24
+
25
+ def self.descendants
26
+ @descendants ||= Set.new
27
+ end
28
+
29
+ module ClassMethods
30
+ def key(*args)
31
+ key = super
32
+ create_indexes_for(key)
33
+ key
34
+ end
35
+
36
+ def ensure_index(name_or_array, options={})
37
+ keys_to_index = if name_or_array.is_a?(Array)
38
+ name_or_array.map { |pair| [pair[0], pair[1]] }
39
+ else
40
+ name_or_array
41
+ end
42
+
43
+ MongoMapper.ensure_index(self, keys_to_index, options)
44
+ end
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
58
+ def find!(*args)
59
+ options = args.extract_options!
60
+ case args.first
61
+ when :first then first(options)
62
+ when :last then last(options)
63
+ when :all then find_every(options)
64
+ when Array then find_some(args, options)
65
+ else
66
+ case args.size
67
+ when 0
68
+ raise DocumentNotFound, "Couldn't find without an ID"
69
+ when 1
70
+ find_one!(options.merge({:_id => args[0]}))
71
+ else
72
+ find_some(args, options)
73
+ end
74
+ end
75
+ end
76
+
77
+ def find(*args)
78
+ find!(*args)
79
+ rescue DocumentNotFound
80
+ nil
81
+ end
82
+
83
+ def paginate(options)
84
+ per_page = options.delete(:per_page) || self.per_page
85
+ page = options.delete(:page)
86
+ total_entries = count(options)
87
+ pagination = Pagination::PaginationProxy.new(total_entries, page, per_page)
88
+
89
+ options.merge!(:limit => pagination.limit, :skip => pagination.skip)
90
+ pagination.subject = find_every(options)
91
+ pagination
92
+ end
93
+
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
+ def first(options={})
102
+ find_one(options)
103
+ end
104
+
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
+ def last(options={})
116
+ raise ':order option must be provided when using last' if options[:order].blank?
117
+ find_one(options.merge(:order => invert_order_clause(options[:order])))
118
+ end
119
+
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
+ def all(options={})
128
+ find_every(options)
129
+ end
130
+
131
+ def find_by_id(id)
132
+ find_one(:_id => id)
133
+ end
134
+
135
+ def count(options={})
136
+ collection.find(to_criteria(options)).count
137
+ end
138
+
139
+ def exists?(options={})
140
+ !count(options).zero?
141
+ end
142
+
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
+ def create(*docs)
162
+ initialize_each(*docs) { |doc| doc.save }
163
+ end
164
+
165
+ # @see Document.create
166
+ #
167
+ # @raise [DocumentNotValid] raised if a document fails to create
168
+ def create!(*docs)
169
+ initialize_each(*docs) { |doc| doc.save! }
170
+ end
171
+
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
+ def update(*args)
190
+ if args.length == 1
191
+ update_multiple(args[0])
192
+ else
193
+ id, attributes = args
194
+ update_single(id, attributes)
195
+ end
196
+ end
197
+
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
+ def delete(*ids)
203
+ collection.remove(to_criteria(:_id => ids.flatten))
204
+ end
205
+
206
+ def delete_all(options={})
207
+ collection.remove(to_criteria(options))
208
+ end
209
+
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
+ def destroy(*ids)
232
+ find_some(ids.flatten).each(&:destroy)
233
+ end
234
+
235
+ def destroy_all(options={})
236
+ all(options).each(&:destroy)
237
+ end
238
+
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
+ def connection(mongo_connection=nil)
248
+ if mongo_connection.nil?
249
+ @connection ||= MongoMapper.connection
250
+ else
251
+ @connection = mongo_connection
252
+ end
253
+ @connection
254
+ end
255
+
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
+ def set_database_name(name)
260
+ @database_name = name
261
+ end
262
+
263
+ # Returns the database name
264
+ #
265
+ # @return [String] the database name
266
+ def database_name
267
+ @database_name
268
+ end
269
+
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
+ def database
275
+ if database_name.nil?
276
+ MongoMapper.database
277
+ else
278
+ connection.db(database_name)
279
+ end
280
+ end
281
+
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
+ def set_collection_name(name)
286
+ @collection_name = name
287
+ end
288
+
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
+ def collection_name
294
+ @collection_name ||= self.to_s.tableize.gsub(/\//, '.')
295
+ end
296
+
297
+ # @return the Mongo Ruby driver +collection+ object
298
+ def collection
299
+ database.collection(collection_name)
300
+ 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.
305
+ def timestamps!
306
+ key :created_at, Time
307
+ key :updated_at, Time
308
+ class_eval { before_save :update_timestamps }
309
+ end
310
+
311
+ def single_collection_inherited?
312
+ keys.has_key?('_type') && single_collection_inherited_superclass?
313
+ end
314
+
315
+ def single_collection_inherited_superclass?
316
+ superclass.respond_to?(:keys) && superclass.keys.has_key?('_type')
317
+ end
318
+
319
+ protected
320
+ def method_missing(method, *args)
321
+ finder = DynamicFinder.new(method)
322
+
323
+ if finder.found?
324
+ meta_def(finder.method) { |*args| dynamic_find(finder, args) }
325
+ send(finder.method, *args)
326
+ else
327
+ super
328
+ end
329
+ end
330
+
331
+ private
332
+ def create_indexes_for(key)
333
+ ensure_index key.name if key.options[:index]
334
+ end
335
+
336
+ def initialize_each(*docs)
337
+ instances = []
338
+ docs = [{}] if docs.blank?
339
+ docs.flatten.each do |attrs|
340
+ doc = initialize_doc(attrs)
341
+ yield(doc)
342
+ instances << doc
343
+ end
344
+ instances.size == 1 ? instances[0] : instances
345
+ end
346
+
347
+ def find_every(options)
348
+ criteria, options = to_finder_options(options)
349
+ collection.find(criteria, options).to_a.map do |doc|
350
+ initialize_doc(doc)
351
+ end
352
+ end
353
+
354
+ def find_some(ids, options={})
355
+ ids = ids.flatten.compact.uniq
356
+ documents = find_every(options.merge(:_id => ids))
357
+
358
+ if ids.size == documents.size
359
+ documents
360
+ else
361
+ raise DocumentNotFound, "Couldn't find all of the ids (#{ids.to_sentence}). Found #{documents.size}, but was expecting #{ids.size}"
362
+ end
363
+ end
364
+
365
+ def find_one(options={})
366
+ criteria, options = to_finder_options(options)
367
+ if doc = collection.find_one(criteria, options)
368
+ initialize_doc(doc)
369
+ end
370
+ end
371
+
372
+ def find_one!(options={})
373
+ find_one(options) || raise(DocumentNotFound, "Document match #{options.inspect} does not exist in #{collection.name} collection")
374
+ end
375
+
376
+ def invert_order_clause(order)
377
+ order.split(',').map do |order_segment|
378
+ if order_segment =~ /\sasc/i
379
+ order_segment.sub /\sasc/i, ' desc'
380
+ elsif order_segment =~ /\sdesc/i
381
+ order_segment.sub /\sdesc/i, ' asc'
382
+ else
383
+ "#{order_segment.strip} desc"
384
+ end
385
+ end.join(',')
386
+ end
387
+
388
+ def update_single(id, attrs)
389
+ if id.blank? || attrs.blank? || !attrs.is_a?(Hash)
390
+ raise ArgumentError, "Updating a single document requires an id and a hash of attributes"
391
+ end
392
+
393
+ doc = find(id)
394
+ doc.update_attributes(attrs)
395
+ doc
396
+ end
397
+
398
+ def update_multiple(docs)
399
+ unless docs.is_a?(Hash)
400
+ raise ArgumentError, "Updating multiple documents takes 1 argument and it must be hash"
401
+ end
402
+
403
+ instances = []
404
+ docs.each_pair { |id, attrs| instances << update(id, attrs) }
405
+ instances
406
+ end
407
+
408
+ def to_criteria(options={})
409
+ FinderOptions.new(self, options).criteria
410
+ end
411
+
412
+ def to_finder_options(options={})
413
+ FinderOptions.new(self, options).to_a
414
+ end
415
+ end
416
+
417
+ module InstanceMethods
418
+ def collection
419
+ self.class.collection
420
+ end
421
+
422
+ def new?
423
+ read_attribute('_id').blank? || using_custom_id?
424
+ end
425
+
426
+ def save(perform_validations=true)
427
+ !perform_validations || valid? ? create_or_update : false
428
+ end
429
+
430
+ def save!
431
+ save || raise(DocumentNotValid.new(self))
432
+ end
433
+
434
+ def destroy
435
+ return false if frozen?
436
+ self.class.delete(_id) unless new?
437
+ freeze
438
+ end
439
+
440
+ def reload
441
+ self.class.find(_id)
442
+ end
443
+
444
+ private
445
+ def create_or_update
446
+ result = new? ? create : update
447
+ result != false
448
+ end
449
+
450
+ def create
451
+ assign_id
452
+ save_to_collection
453
+ end
454
+
455
+ def assign_id
456
+ if read_attribute(:_id).blank?
457
+ write_attribute :_id, Mongo::ObjectID.new
458
+ end
459
+ end
460
+
461
+ def update
462
+ save_to_collection
463
+ end
464
+
465
+ def save_to_collection
466
+ clear_custom_id_flag
467
+ collection.save(to_mongo)
468
+ end
469
+
470
+ def update_timestamps
471
+ now = Time.now.utc
472
+ write_attribute('created_at', now) if new?
473
+ write_attribute('updated_at', now)
474
+ end
475
+
476
+ def clear_custom_id_flag
477
+ @using_custom_id = nil
478
+ end
479
+ end
480
+ end # Document
481
+ end # MongoMapper