danielharan-mongo_mapper 0.6.5

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 (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