djsun-mongomapper 0.3.5.5 → 0.4.1.2

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