djsun-mongomapper 0.3.5.5 → 0.4.1.2

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 (44) hide show
  1. data/README.rdoc +38 -38
  2. data/Rakefile +87 -73
  3. data/VERSION +1 -1
  4. data/lib/mongomapper.rb +67 -71
  5. data/lib/mongomapper/associations.rb +86 -84
  6. data/lib/mongomapper/associations/belongs_to_polymorphic_proxy.rb +34 -34
  7. data/lib/mongomapper/associations/many_embedded_proxy.rb +67 -17
  8. data/lib/mongomapper/associations/proxy.rb +74 -73
  9. data/lib/mongomapper/document.rb +342 -348
  10. data/lib/mongomapper/embedded_document.rb +354 -274
  11. data/lib/mongomapper/finder_options.rb +84 -84
  12. data/lib/mongomapper/key.rb +32 -76
  13. data/lib/mongomapper/rails_compatibility/document.rb +14 -14
  14. data/lib/mongomapper/rails_compatibility/embedded_document.rb +26 -24
  15. data/lib/mongomapper/support.rb +156 -29
  16. data/lib/mongomapper/validations.rb +69 -47
  17. data/test/custom_matchers.rb +48 -0
  18. data/test/functional/associations/test_belongs_to_polymorphic_proxy.rb +53 -56
  19. data/test/functional/associations/test_belongs_to_proxy.rb +48 -49
  20. data/test/functional/associations/test_many_documents_as_proxy.rb +208 -253
  21. data/test/functional/associations/test_many_embedded_polymorphic_proxy.rb +130 -130
  22. data/test/functional/associations/test_many_embedded_proxy.rb +168 -106
  23. data/test/functional/associations/test_many_polymorphic_proxy.rb +261 -262
  24. data/test/functional/test_binary.rb +21 -0
  25. data/test/functional/test_document.rb +946 -952
  26. data/test/functional/test_embedded_document.rb +98 -0
  27. data/test/functional/test_pagination.rb +87 -80
  28. data/test/functional/test_rails_compatibility.rb +29 -29
  29. data/test/functional/test_validations.rb +262 -172
  30. data/test/models.rb +169 -169
  31. data/test/test_helper.rb +28 -66
  32. data/test/unit/serializers/test_json_serializer.rb +193 -193
  33. data/test/unit/test_document.rb +161 -123
  34. data/test/unit/test_embedded_document.rb +643 -547
  35. data/test/unit/test_finder_options.rb +183 -183
  36. data/test/unit/test_key.rb +175 -247
  37. data/test/unit/test_rails_compatibility.rb +38 -33
  38. data/test/unit/test_serializations.rb +52 -52
  39. data/test/unit/test_support.rb +268 -0
  40. data/test/unit/test_time_zones.rb +40 -0
  41. data/test/unit/test_validations.rb +499 -258
  42. metadata +22 -12
  43. data/History +0 -76
  44. data/mongomapper.gemspec +0 -145
@@ -1,34 +1,34 @@
1
- module MongoMapper
2
- module Associations
3
- class BelongsToPolymorphicProxy < Proxy
4
- def replace(doc)
5
- if doc
6
- doc.save if doc.new?
7
- id, type = doc.id, doc.class.name
8
- end
9
-
10
- @owner.send("#{@association.foreign_key}=", id)
11
- @owner.send("#{@association.type_key_name}=", type)
12
- reset
13
- end
14
-
15
- protected
16
- def find_target
17
- if proxy_id && proxy_class
18
- proxy_class.find_by_id(proxy_id)
19
- end
20
- end
21
-
22
- def proxy_id
23
- @proxy_id ||= @owner.send(@association.foreign_key)
24
- end
25
-
26
- def proxy_class
27
- @proxy_class ||= begin
28
- klass = @owner.send(@association.type_key_name)
29
- klass && klass.constantize
30
- end
31
- end
32
- end
33
- end
34
- end
1
+ module MongoMapper
2
+ module Associations
3
+ class BelongsToPolymorphicProxy < Proxy
4
+ def replace(doc)
5
+ if doc
6
+ doc.save if doc.new?
7
+ id, type = doc.id, doc.class.name
8
+ end
9
+
10
+ @owner.send("#{@association.foreign_key}=", id)
11
+ @owner.send("#{@association.type_key_name}=", type)
12
+ reset
13
+ end
14
+
15
+ protected
16
+ def find_target
17
+ if proxy_id && proxy_class
18
+ proxy_class.find_by_id(proxy_id)
19
+ end
20
+ end
21
+
22
+ def proxy_id
23
+ @proxy_id ||= @owner.send(@association.foreign_key)
24
+ end
25
+
26
+ def proxy_class
27
+ @proxy_class ||= begin
28
+ klass = @owner.send(@association.type_key_name)
29
+ klass && klass.constantize
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,17 +1,67 @@
1
- module MongoMapper
2
- module Associations
3
- class ManyEmbeddedProxy < Proxy
4
- def replace(v)
5
- @_values = v.map { |e| e.kind_of?(EmbeddedDocument) ? e.attributes : e }
6
- reset
7
- end
8
-
9
- protected
10
- def find_target
11
- (@_values || []).map do |e|
12
- @association.klass.new(e)
13
- end
14
- end
15
- end
16
- end
17
- end
1
+ module MongoMapper
2
+ module Associations
3
+ class ManyEmbeddedProxy < Proxy
4
+ def replace(v)
5
+ @_values = v.map { |e| e.kind_of?(EmbeddedDocument) ? e.attributes : e }
6
+ reset
7
+ end
8
+
9
+ def build(opts={})
10
+ owner = @owner
11
+ child = @association.klass.new(opts)
12
+ assign_parent_reference(child)
13
+ child._root_document = owner
14
+ self << child
15
+ child
16
+ end
17
+
18
+ def find(opts)
19
+ case opts
20
+ when :all
21
+ self
22
+ when String
23
+ if load_target
24
+ child = @target.detect {|item| item.id == opts}
25
+ assign_parent_reference(child)
26
+ child
27
+ end
28
+ end
29
+ end
30
+
31
+ def <<(*docs)
32
+ if load_target
33
+ root = @owner._root_document || @owner
34
+ docs.each do |doc|
35
+ doc._root_document = root
36
+ @target << doc
37
+ end
38
+ end
39
+ end
40
+ alias_method :push, :<<
41
+ alias_method :concat, :<<
42
+
43
+ protected
44
+ def find_target
45
+ (@_values || []).map do |e|
46
+ child = @association.klass.new(e)
47
+ assign_parent_reference(child)
48
+ child
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def assign_parent_reference(child)
55
+ return unless child && @owner
56
+ return if @owner.class.name.blank?
57
+ owner = @owner
58
+ child.class_eval do
59
+ define_method(owner.class.name.underscore) do
60
+ owner
61
+ end
62
+ end
63
+ end
64
+
65
+ end
66
+ end
67
+ end
@@ -1,73 +1,74 @@
1
- module MongoMapper
2
- module Associations
3
- class Proxy < BasicObject
4
- attr_reader :owner, :association
5
-
6
- def initialize(owner, association)
7
- @owner = owner
8
- @association = association
9
- reset
10
- end
11
-
12
- def respond_to?(*methods)
13
- (load_target && @target.respond_to?(*methods))
14
- end
15
-
16
- def reset
17
- @target = nil
18
- end
19
-
20
- def reload_target
21
- reset
22
- load_target
23
- self
24
- end
25
-
26
- def send(method, *args)
27
- load_target
28
- @target.send(method, *args)
29
- end
30
-
31
- def replace(v)
32
- raise NotImplementedError
33
- end
34
-
35
- def inspect
36
- load_target
37
- @target.inspect
38
- end
39
-
40
- def nil?
41
- load_target
42
- @target.nil?
43
- end
44
-
45
- protected
46
- def method_missing(method, *args)
47
- if load_target
48
- if block_given?
49
- @target.send(method, *args) { |*block_args| yield(*block_args) }
50
- else
51
- @target.send(method, *args)
52
- end
53
- end
54
- end
55
-
56
- def load_target
57
- @target ||= find_target
58
- end
59
-
60
- def find_target
61
- raise NotImplementedError
62
- end
63
-
64
- # Array#flatten has problems with recursive arrays. Going one level
65
- # deeper solves the majority of the problems.
66
- def flatten_deeper(array)
67
- array.collect do |element|
68
- (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element
69
- end.flatten
70
- end
71
- end
72
- end
73
- end
1
+ module MongoMapper
2
+ module Associations
3
+ class Proxy < BasicObject
4
+ attr_reader :owner, :association
5
+
6
+ def initialize(owner, association)
7
+ @owner = owner
8
+ @association = association
9
+ reset
10
+ end
11
+
12
+ def respond_to?(*methods)
13
+ (load_target && @target.respond_to?(*methods))
14
+ end
15
+
16
+ def reset
17
+ @target = nil
18
+ end
19
+
20
+ def reload_target
21
+ reset
22
+ load_target
23
+ self
24
+ end
25
+
26
+ def send(method, *args)
27
+ return super if methods.include? method.to_s
28
+ load_target
29
+ @target.send(method, *args)
30
+ end
31
+
32
+ def replace(v)
33
+ raise NotImplementedError
34
+ end
35
+
36
+ def inspect
37
+ load_target
38
+ @target.inspect
39
+ end
40
+
41
+ def nil?
42
+ load_target
43
+ @target.nil?
44
+ end
45
+
46
+ protected
47
+ def method_missing(method, *args)
48
+ if load_target
49
+ if block_given?
50
+ @target.send(method, *args) { |*block_args| yield(*block_args) }
51
+ else
52
+ @target.send(method, *args)
53
+ end
54
+ end
55
+ end
56
+
57
+ def load_target
58
+ @target ||= find_target
59
+ end
60
+
61
+ def find_target
62
+ raise NotImplementedError
63
+ end
64
+
65
+ # Array#flatten has problems with recursive arrays. Going one level
66
+ # deeper solves the majority of the problems.
67
+ def flatten_deeper(array)
68
+ array.collect do |element|
69
+ (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element
70
+ end.flatten
71
+ end
72
+ end
73
+ end
74
+ end
@@ -1,348 +1,342 @@
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 SaveWithValidation
12
- include RailsCompatibility::Document
13
- extend ClassMethods
14
- end
15
-
16
- descendants << model
17
- end
18
-
19
- def self.descendants
20
- @descendants ||= Set.new
21
- end
22
-
23
- module ClassMethods
24
- def find(*args)
25
- options = args.extract_options!
26
-
27
- case args.first
28
- when :first then find_first(options)
29
- when :last then find_last(options)
30
- when :all then find_every(options)
31
- else find_from_ids(args, options)
32
- end
33
- end
34
-
35
- def paginate(options)
36
- per_page = options.delete(:per_page)
37
- page = options.delete(:page)
38
- total_entries = count(options[:conditions] || {})
39
- collection = Pagination::PaginationProxy.new(total_entries, page, per_page)
40
-
41
- options[:limit] = collection.limit
42
- options[:offset] = collection.offset
43
-
44
- collection.subject = find_every(options)
45
- collection
46
- end
47
-
48
- def first(options={})
49
- find_first(options)
50
- end
51
-
52
- def last(options={})
53
- find_last(options)
54
- end
55
-
56
- def all(options={})
57
- find_every(options)
58
- end
59
-
60
- def find_by_id(id)
61
- criteria = FinderOptions.to_mongo_criteria(:_id => id)
62
- if doc = collection.find_one(criteria)
63
- new(doc)
64
- end
65
- end
66
-
67
- def count(conditions={})
68
- collection.find(FinderOptions.to_mongo_criteria(conditions)).count
69
- end
70
-
71
- def create(*docs)
72
- instances = []
73
- docs = [{}] if docs.blank?
74
- docs.flatten.each do |attrs|
75
- doc = new(attrs); doc.save
76
- instances << doc
77
- end
78
- instances.size == 1 ? instances[0] : instances
79
- end
80
-
81
- # For updating single document
82
- # Person.update(1, {:foo => 'bar'})
83
- #
84
- # For updating multiple documents at once:
85
- # Person.update({'1' => {:foo => 'bar'}, '2' => {:baz => 'wick'}})
86
- def update(*args)
87
- updating_multiple = args.length == 1
88
- if updating_multiple
89
- update_multiple(args[0])
90
- else
91
- id, attributes = args
92
- update_single(id, attributes)
93
- end
94
- end
95
-
96
- def delete(*ids)
97
- criteria = FinderOptions.to_mongo_criteria(:_id => ids.flatten)
98
- collection.remove(criteria)
99
- end
100
-
101
- def delete_all(conditions={})
102
- criteria = FinderOptions.to_mongo_criteria(conditions)
103
- collection.remove(criteria)
104
- end
105
-
106
- def destroy(*ids)
107
- find_some(ids.flatten).each(&:destroy)
108
- end
109
-
110
- def destroy_all(conditions={})
111
- find(:all, :conditions => conditions).each(&:destroy)
112
- end
113
-
114
- def connection(mongo_connection=nil)
115
- if mongo_connection.nil?
116
- @connection ||= MongoMapper.connection
117
- else
118
- @connection = mongo_connection
119
- end
120
- @connection
121
- end
122
-
123
- def database(name=nil)
124
- if name.nil?
125
- @database ||= MongoMapper.database
126
- else
127
- @database = connection.db(name)
128
- end
129
- @database
130
- end
131
-
132
- def collection(name=nil)
133
- if name.nil?
134
- @collection ||= database.collection(self.to_s.demodulize.tableize)
135
- else
136
- @collection = database.collection(name)
137
- end
138
- @collection
139
- end
140
-
141
- def timestamps!
142
- key :created_at, Time
143
- key :updated_at, Time
144
-
145
- class_eval { before_save :update_timestamps }
146
- end
147
-
148
- def validates_uniqueness_of(*args)
149
- add_validations(args, MongoMapper::Validations::ValidatesUniquenessOf)
150
- end
151
-
152
- def validates_exclusion_of(*args)
153
- add_validations(args, MongoMapper::Validations::ValidatesExclusionOf)
154
- end
155
-
156
- def validates_inclusion_of(*args)
157
- add_validations(args, MongoMapper::Validations::ValidatesInclusionOf)
158
- end
159
-
160
- protected
161
- def method_missing(method, *args)
162
- finder = DynamicFinder.new(self, method)
163
-
164
- if finder.valid?
165
- meta_def(finder.options[:method]) do |*args|
166
- find_with_args(args, finder.options)
167
- end
168
-
169
- send(finder.options[:method], *args)
170
- else
171
- super
172
- end
173
- end
174
-
175
- private
176
- def find_every(options)
177
- criteria, options = FinderOptions.new(options).to_a
178
- collection.find(criteria, options).to_a.map { |doc| new(doc) }
179
- end
180
-
181
- def find_first(options)
182
- options.merge!(:limit => 1)
183
- find_every({:order => '$natural asc'}.merge(options))[0]
184
- end
185
-
186
- def find_last(options)
187
- options.merge!(:limit => 1)
188
- options[:order] = invert_order_clause(options)
189
- find_every(options)[0]
190
- #find_every({:order => '$natural desc'}.merge(invert_order_clause(options)))[0]
191
- end
192
-
193
- def invert_order_clause(options)
194
- return '$natural desc' unless options[:order]
195
- options[:order].split(',').map do |order_segment|
196
- if order_segment =~ /\sasc/i
197
- order_segment.sub /\sasc/i, ' desc'
198
- elsif order_segment =~ /\sdesc/i
199
- order_segment.sub /\sdesc/i, ' asc'
200
- else
201
- "#{order_segment.strip} desc"
202
- end
203
- end.join(',')
204
- end
205
-
206
- def find_some(ids, options={})
207
- documents = find_every(options.deep_merge(:conditions => {'_id' => ids}))
208
- if ids.size == documents.size
209
- documents
210
- else
211
- raise DocumentNotFound, "Couldn't find all of the ids (#{ids.to_sentence}). Found #{documents.size}, but was expecting #{ids.size}"
212
- end
213
- end
214
-
215
- def find_one(id, options={})
216
- if doc = find_every(options.deep_merge(:conditions => {:_id => id})).first
217
- doc
218
- else
219
- raise DocumentNotFound, "Document with id of #{id} does not exist in collection named #{collection.name}"
220
- end
221
- end
222
-
223
- def find_from_ids(ids, options={})
224
- ids = ids.flatten.compact.uniq
225
-
226
- case ids.size
227
- when 0
228
- raise(DocumentNotFound, "Couldn't find without an ID")
229
- when 1
230
- find_one(ids[0], options)
231
- else
232
- find_some(ids, options)
233
- end
234
- end
235
-
236
- def find_with_args(args, options)
237
- attributes, = {}
238
- find_options = args.extract_options!.deep_merge(:conditions => attributes)
239
-
240
- options[:attribute_names].each_with_index do |attr, index|
241
- attributes[attr] = args[index]
242
- end
243
-
244
- result = find(options[:finder], find_options)
245
-
246
- if result.nil?
247
- if options[:bang]
248
- raise DocumentNotFound, "Couldn't find Document with #{attributes.inspect} in collection named #{collection.name}"
249
- end
250
-
251
- if options[:instantiator]
252
- self.send(options[:instantiator], attributes)
253
- end
254
- else
255
- result
256
- end
257
- end
258
-
259
- def update_single(id, attrs)
260
- if id.blank? || attrs.blank? || !attrs.is_a?(Hash)
261
- raise ArgumentError, "Updating a single document requires an id and a hash of attributes"
262
- end
263
-
264
- doc = find(id)
265
- doc.update_attributes(attrs)
266
- doc
267
- end
268
-
269
- def update_multiple(docs)
270
- unless docs.is_a?(Hash)
271
- raise ArgumentError, "Updating multiple documents takes 1 argument and it must be hash"
272
- end
273
-
274
- instances = []
275
- docs.each_pair { |id, attrs| instances << update(id, attrs) }
276
- instances
277
- end
278
- end
279
-
280
- module InstanceMethods
281
- def collection
282
- self.class.collection
283
- end
284
-
285
- def new?
286
- read_attribute('_id').blank? || using_custom_id?
287
- end
288
-
289
- def save
290
- create_or_update
291
- end
292
-
293
- def save!
294
- create_or_update || raise(DocumentNotValid.new(self))
295
- end
296
-
297
- def update_attributes(attrs={})
298
- self.attributes = attrs
299
- save
300
- end
301
-
302
- def destroy
303
- return false if frozen?
304
-
305
- criteria = FinderOptions.to_mongo_criteria(:_id => id)
306
- collection.remove(criteria) unless new?
307
- freeze
308
- end
309
-
310
- private
311
- def create_or_update
312
- result = new? ? create : update
313
- result != false
314
- end
315
-
316
- def create
317
- assign_id
318
- save_to_collection
319
- end
320
-
321
- def assign_id
322
- if read_attribute(:_id).blank?
323
- write_attribute(:_id, Mongo::ObjectID.new.to_s)
324
- end
325
- end
326
-
327
- def update
328
- save_to_collection
329
- end
330
-
331
- # collection.save returns mongoid
332
- def save_to_collection
333
- clear_custom_id_flag
334
- collection.save(attributes)
335
- end
336
-
337
- def update_timestamps
338
- now = Time.now.utc
339
- write_attribute('created_at', now) if new?
340
- write_attribute('updated_at', now)
341
- end
342
-
343
- def clear_custom_id_flag
344
- @using_custom_id = nil
345
- end
346
- end
347
- end # Document
348
- end # MongoMapper
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 SaveWithValidation
12
+ include RailsCompatibility::Document
13
+ extend Validations::Macros
14
+ extend ClassMethods
15
+
16
+ def self.per_page
17
+ 25
18
+ end unless respond_to?(:per_page)
19
+ end
20
+
21
+ descendants << model
22
+ end
23
+
24
+ def self.descendants
25
+ @descendants ||= Set.new
26
+ end
27
+
28
+ module ClassMethods
29
+ def find(*args)
30
+ options = args.extract_options!
31
+
32
+ case args.first
33
+ when :first then find_first(options)
34
+ when :last then find_last(options)
35
+ when :all then find_every(options)
36
+ else find_from_ids(args, options)
37
+ end
38
+ end
39
+
40
+ def paginate(options)
41
+ per_page = options.delete(:per_page) || self.per_page
42
+ page = options.delete(:page)
43
+ total_entries = count(options[:conditions] || {})
44
+ collection = Pagination::PaginationProxy.new(total_entries, page, per_page)
45
+
46
+ options[:limit] = collection.limit
47
+ options[:skip] = collection.skip
48
+
49
+ collection.subject = find_every(options)
50
+ collection
51
+ end
52
+
53
+ def first(options={})
54
+ find_first(options)
55
+ end
56
+
57
+ def last(options={})
58
+ find_last(options)
59
+ end
60
+
61
+ def all(options={})
62
+ find_every(options)
63
+ end
64
+
65
+ def find_by_id(id)
66
+ criteria = FinderOptions.to_mongo_criteria(:_id => id)
67
+ if doc = collection.find_one(criteria)
68
+ new(doc)
69
+ end
70
+ end
71
+
72
+ def count(conditions={})
73
+ collection.find(FinderOptions.to_mongo_criteria(conditions)).count
74
+ end
75
+
76
+ def create(*docs)
77
+ instances = []
78
+ docs = [{}] if docs.blank?
79
+ docs.flatten.each do |attrs|
80
+ doc = new(attrs); doc.save
81
+ instances << doc
82
+ end
83
+ instances.size == 1 ? instances[0] : instances
84
+ end
85
+
86
+ # For updating single document
87
+ # Person.update(1, {:foo => 'bar'})
88
+ #
89
+ # For updating multiple documents at once:
90
+ # Person.update({'1' => {:foo => 'bar'}, '2' => {:baz => 'wick'}})
91
+ def update(*args)
92
+ updating_multiple = args.length == 1
93
+ if updating_multiple
94
+ update_multiple(args[0])
95
+ else
96
+ id, attributes = args
97
+ update_single(id, attributes)
98
+ end
99
+ end
100
+
101
+ def delete(*ids)
102
+ criteria = FinderOptions.to_mongo_criteria(:_id => ids.flatten)
103
+ collection.remove(criteria)
104
+ end
105
+
106
+ def delete_all(conditions={})
107
+ criteria = FinderOptions.to_mongo_criteria(conditions)
108
+ collection.remove(criteria)
109
+ end
110
+
111
+ def destroy(*ids)
112
+ find_some(ids.flatten).each(&:destroy)
113
+ end
114
+
115
+ def destroy_all(conditions={})
116
+ find(:all, :conditions => conditions).each(&:destroy)
117
+ end
118
+
119
+ def connection(mongo_connection=nil)
120
+ if mongo_connection.nil?
121
+ @connection ||= MongoMapper.connection
122
+ else
123
+ @connection = mongo_connection
124
+ end
125
+ @connection
126
+ end
127
+
128
+ def database(name=nil)
129
+ if name.nil?
130
+ @database ||= MongoMapper.database
131
+ else
132
+ @database = connection.db(name)
133
+ end
134
+ @database
135
+ end
136
+
137
+ # Changes the collection name from the default to whatever you want
138
+ def set_collection_name(name=nil)
139
+ @collection = nil
140
+ @collection_name = name
141
+ end
142
+
143
+ # Returns the collection name, if not set, defaults to class name tableized
144
+ def collection_name
145
+ @collection_name ||= self.to_s.demodulize.tableize
146
+ end
147
+
148
+ # Returns the mongo ruby driver collection object
149
+ def collection
150
+ @collection ||= database.collection(collection_name)
151
+ end
152
+
153
+ def timestamps!
154
+ key :created_at, Time
155
+ key :updated_at, Time
156
+
157
+ class_eval { before_save :update_timestamps }
158
+ end
159
+
160
+ protected
161
+ def method_missing(method, *args)
162
+ finder = DynamicFinder.new(self, method)
163
+
164
+ if finder.valid?
165
+ meta_def(finder.options[:method]) do |*args|
166
+ find_with_args(args, finder.options)
167
+ end
168
+
169
+ send(finder.options[:method], *args)
170
+ else
171
+ super
172
+ end
173
+ end
174
+
175
+ private
176
+ def find_every(options)
177
+ criteria, options = FinderOptions.new(options).to_a
178
+ collection.find(criteria, options).to_a.map { |doc| new(doc) }
179
+ end
180
+
181
+ def find_first(options)
182
+ options.merge!(:limit => 1)
183
+ find_every({:order => '$natural asc'}.merge(options))[0]
184
+ end
185
+
186
+ def find_last(options)
187
+ options.merge!(:limit => 1)
188
+ options[:order] = invert_order_clause(options)
189
+ find_every(options)[0]
190
+ #find_every({:order => '$natural desc'}.merge(invert_order_clause(options)))[0]
191
+ end
192
+
193
+ def invert_order_clause(options)
194
+ return '$natural desc' unless options[:order]
195
+ options[:order].split(',').map do |order_segment|
196
+ if order_segment =~ /\sasc/i
197
+ order_segment.sub /\sasc/i, ' desc'
198
+ elsif order_segment =~ /\sdesc/i
199
+ order_segment.sub /\sdesc/i, ' asc'
200
+ else
201
+ "#{order_segment.strip} desc"
202
+ end
203
+ end.join(',')
204
+ end
205
+
206
+ def find_some(ids, options={})
207
+ documents = find_every(options.deep_merge(:conditions => {'_id' => ids}))
208
+ if ids.size == documents.size
209
+ documents
210
+ else
211
+ raise DocumentNotFound, "Couldn't find all of the ids (#{ids.to_sentence}). Found #{documents.size}, but was expecting #{ids.size}"
212
+ end
213
+ end
214
+
215
+ def find_one(id, options={})
216
+ if doc = find_every(options.deep_merge(:conditions => {:_id => id})).first
217
+ doc
218
+ else
219
+ raise DocumentNotFound, "Document with id of #{id} does not exist in collection named #{collection.name}"
220
+ end
221
+ end
222
+
223
+ def find_from_ids(ids, options={})
224
+ ids = ids.flatten.compact.uniq
225
+
226
+ case ids.size
227
+ when 0
228
+ raise(DocumentNotFound, "Couldn't find without an ID")
229
+ when 1
230
+ find_one(ids[0], options)
231
+ else
232
+ find_some(ids, options)
233
+ end
234
+ end
235
+
236
+ def find_with_args(args, options)
237
+ attributes, = {}
238
+ find_options = args.extract_options!.deep_merge(:conditions => attributes)
239
+
240
+ options[:attribute_names].each_with_index do |attr, index|
241
+ attributes[attr] = args[index]
242
+ end
243
+
244
+ result = find(options[:finder], find_options)
245
+
246
+ if result.nil?
247
+ if options[:bang]
248
+ raise DocumentNotFound, "Couldn't find Document with #{attributes.inspect} in collection named #{collection.name}"
249
+ end
250
+
251
+ if options[:instantiator]
252
+ self.send(options[:instantiator], attributes)
253
+ end
254
+ else
255
+ result
256
+ end
257
+ end
258
+
259
+ def update_single(id, attrs)
260
+ if id.blank? || attrs.blank? || !attrs.is_a?(Hash)
261
+ raise ArgumentError, "Updating a single document requires an id and a hash of attributes"
262
+ end
263
+
264
+ doc = find(id)
265
+ doc.update_attributes(attrs)
266
+ doc
267
+ end
268
+
269
+ def update_multiple(docs)
270
+ unless docs.is_a?(Hash)
271
+ raise ArgumentError, "Updating multiple documents takes 1 argument and it must be hash"
272
+ end
273
+
274
+ instances = []
275
+ docs.each_pair { |id, attrs| instances << update(id, attrs) }
276
+ instances
277
+ end
278
+ end
279
+
280
+ module InstanceMethods
281
+ def collection
282
+ self.class.collection
283
+ end
284
+
285
+ def new?
286
+ read_attribute('_id').blank? || using_custom_id?
287
+ end
288
+
289
+ def save
290
+ create_or_update
291
+ end
292
+
293
+ def save!
294
+ create_or_update || raise(DocumentNotValid.new(self))
295
+ end
296
+
297
+ def destroy
298
+ return false if frozen?
299
+
300
+ criteria = FinderOptions.to_mongo_criteria(:_id => id)
301
+ collection.remove(criteria) unless new?
302
+ freeze
303
+ end
304
+
305
+ private
306
+ def create_or_update
307
+ result = new? ? create : update
308
+ result != false
309
+ end
310
+
311
+ def create
312
+ assign_id
313
+ save_to_collection
314
+ end
315
+
316
+ def assign_id
317
+ if read_attribute(:_id).blank?
318
+ write_attribute(:_id, Mongo::ObjectID.new.to_s)
319
+ end
320
+ end
321
+
322
+ def update
323
+ save_to_collection
324
+ end
325
+
326
+ def save_to_collection
327
+ clear_custom_id_flag
328
+ collection.save(to_mongo)
329
+ end
330
+
331
+ def update_timestamps
332
+ now = Time.now.utc
333
+ write_attribute('created_at', now) if new?
334
+ write_attribute('updated_at', now)
335
+ end
336
+
337
+ def clear_custom_id_flag
338
+ @using_custom_id = nil
339
+ end
340
+ end
341
+ end # Document
342
+ end # MongoMapper