mongo_mapper 0.5.6 → 0.5.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/.gitignore +3 -1
  2. data/README.rdoc +3 -0
  3. data/VERSION +1 -1
  4. data/lib/mongo_mapper.rb +14 -6
  5. data/lib/mongo_mapper/associations.rb +11 -5
  6. data/lib/mongo_mapper/associations/base.rb +17 -5
  7. data/lib/mongo_mapper/associations/many_documents_as_proxy.rb +0 -2
  8. data/lib/mongo_mapper/associations/many_documents_proxy.rb +15 -15
  9. data/lib/mongo_mapper/associations/many_embedded_polymorphic_proxy.rb +2 -2
  10. data/lib/mongo_mapper/associations/many_polymorphic_proxy.rb +1 -1
  11. data/lib/mongo_mapper/associations/proxy.rb +1 -0
  12. data/lib/mongo_mapper/callbacks.rb +18 -0
  13. data/lib/mongo_mapper/document.rb +206 -89
  14. data/lib/mongo_mapper/dynamic_finder.rb +1 -1
  15. data/lib/mongo_mapper/embedded_document.rb +7 -3
  16. data/lib/mongo_mapper/finder_options.rb +87 -66
  17. data/lib/mongo_mapper/pagination.rb +2 -0
  18. data/lib/mongo_mapper/serialization.rb +2 -3
  19. data/lib/mongo_mapper/serializers/json_serializer.rb +1 -1
  20. data/lib/mongo_mapper/support.rb +9 -0
  21. data/lib/mongo_mapper/validations.rb +3 -1
  22. data/mongo_mapper.gemspec +4 -4
  23. data/test/functional/associations/test_many_documents_as_proxy.rb +2 -2
  24. data/test/functional/associations/test_many_embedded_polymorphic_proxy.rb +25 -1
  25. data/test/functional/associations/test_many_embedded_proxy.rb +25 -0
  26. data/test/functional/associations/test_many_polymorphic_proxy.rb +48 -6
  27. data/test/functional/associations/test_many_proxy.rb +27 -6
  28. data/test/functional/test_document.rb +49 -29
  29. data/test/functional/test_pagination.rb +17 -17
  30. data/test/functional/test_validations.rb +35 -14
  31. data/test/models.rb +85 -10
  32. data/test/support/{test_timing.rb → timing.rb} +1 -1
  33. data/test/test_helper.rb +8 -8
  34. data/test/unit/test_association_base.rb +17 -0
  35. data/test/unit/test_document.rb +12 -1
  36. data/test/unit/test_embedded_document.rb +13 -4
  37. data/test/unit/test_finder_options.rb +50 -48
  38. data/test/unit/test_pagination.rb +4 -0
  39. metadata +4 -4
data/.gitignore CHANGED
@@ -5,4 +5,6 @@ rdoc
5
5
  pkg
6
6
  *~
7
7
  *.gem
8
- tmp
8
+ tmp
9
+ .yardoc
10
+ doc/*
@@ -35,7 +35,10 @@ You can also just declare the source:
35
35
 
36
36
  == Documentation
37
37
 
38
+ Documentation is lacking right now because if you can't look through the code right now and feel comfortable, this is probably too young for you to use. Wait til it stabilizes a bit more.
39
+
38
40
  http://rdoc.info/projects/jnunemaker/mongomapper
41
+ http://groups.google.com/group/mongomapper
39
42
 
40
43
  == More Info
41
44
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.6
1
+ 0.5.7
@@ -3,8 +3,17 @@ require 'mongo'
3
3
  require 'validatable'
4
4
 
5
5
  module MongoMapper
6
- DocumentNotFound = Class.new(StandardError)
7
- DocumentNotValid = Class.new(StandardError) do
6
+ # generic MM error
7
+ class MongoMapperError < StandardError; end
8
+
9
+ # raised when key expected to exist but not found
10
+ class KeyNotFound < MongoMapperError; end
11
+
12
+ # raised when document expected but not found
13
+ class DocumentNotFound < MongoMapperError; end
14
+
15
+ # raised when document not valid and using !
16
+ class DocumentNotValid < MongoMapperError
8
17
  def initialize(document)
9
18
  @document = document
10
19
  super("Validation failed: #{@document.errors.full_messages.join(", ")}")
@@ -54,13 +63,12 @@ module MongoMapper
54
63
  module Finders
55
64
  def dynamic_find(finder, args)
56
65
  attributes = {}
57
- find_options = args.extract_options!.deep_merge(:conditions => attributes)
58
-
59
66
  finder.attributes.each_with_index do |attr, index|
60
67
  attributes[attr] = args[index]
61
68
  end
62
-
63
- result = find(finder.finder, find_options)
69
+
70
+ options = args.extract_options!.merge(attributes)
71
+ result = find(finder.finder, options)
64
72
 
65
73
  if result.nil?
66
74
  if finder.bang
@@ -1,13 +1,13 @@
1
1
  module MongoMapper
2
2
  module Associations
3
3
  module ClassMethods
4
- def belongs_to(association_id, options = {})
4
+ def belongs_to(association_id, options={})
5
5
  create_association(:belongs_to, association_id, options)
6
6
  self
7
7
  end
8
8
 
9
- def many(association_id, options = {})
10
- create_association(:many, association_id, options)
9
+ def many(association_id, options = {}, &block)
10
+ create_association(:many, association_id, options, &block)
11
11
  self
12
12
  end
13
13
 
@@ -18,13 +18,20 @@ module MongoMapper
18
18
  end
19
19
 
20
20
  private
21
- def create_association(type, name, options)
21
+ def create_association(type, name, options, &extension)
22
+ options[:extend] = modulized_extensions(extension, options[:extend])
22
23
  association = Associations::Base.new(type, name, options)
23
24
  associations[association.name] = association
24
25
  define_association_methods(association)
25
26
  define_dependent_callback(association)
26
27
  association
27
28
  end
29
+
30
+ def modulized_extensions(*extensions)
31
+ extensions.flatten.compact.map do |extension|
32
+ Proc === extension ? Module.new(&extension) : extension
33
+ end
34
+ end
28
35
 
29
36
  def define_association_methods(association)
30
37
  define_method(association.name) do
@@ -59,7 +66,6 @@ module MongoMapper
59
66
  end
60
67
  end
61
68
  end
62
-
63
69
  end
64
70
 
65
71
  module InstanceMethods
@@ -1,10 +1,22 @@
1
1
  module MongoMapper
2
2
  module Associations
3
3
  class Base
4
- attr_reader :type, :name, :options
5
-
6
- def initialize(type, name, options = {})
7
- @type, @name, @options = type, name, options
4
+ attr_reader :type, :name, :options, :finder_options
5
+
6
+ # Options that should not be considered MongoDB query options/criteria
7
+ AssociationOptions = [:as, :class_name, :dependent, :extend, :foreign_key, :polymorphic]
8
+
9
+ def initialize(type, name, options={})
10
+ @type, @name = type, name
11
+ @options, @finder_options = {}, {}
12
+
13
+ options.each_pair do |key, value|
14
+ if AssociationOptions.include?(key)
15
+ @options[key] = value
16
+ else
17
+ @finder_options[key] = value
18
+ end
19
+ end
8
20
  end
9
21
 
10
22
  def class_name
@@ -26,7 +38,7 @@ module MongoMapper
26
38
  def many?
27
39
  @many_type ||= @type == :many
28
40
  end
29
-
41
+
30
42
  def belongs_to?
31
43
  @belongs_to_type ||= @type == :belongs_to
32
44
  end
@@ -8,10 +8,8 @@ module MongoMapper
8
8
 
9
9
  def apply_scope(doc)
10
10
  ensure_owner_saved
11
-
12
11
  doc.send("#{as_type_name}=", @owner.class.name)
13
12
  doc.send("#{as_id_name}=", @owner.id)
14
-
15
13
  doc
16
14
  end
17
15
 
@@ -4,7 +4,7 @@ module MongoMapper
4
4
  delegate :klass, :to => :@association
5
5
  delegate :collection, :to => :klass
6
6
 
7
- include MongoMapper::Finders
7
+ include ::MongoMapper::Finders
8
8
 
9
9
  def find(*args)
10
10
  options = args.extract_options!
@@ -16,19 +16,19 @@ module MongoMapper
16
16
  end
17
17
 
18
18
  def all(options={})
19
- find(:all, scoped_options(options))
19
+ klass.all(scoped_options(options))
20
20
  end
21
21
 
22
22
  def first(options={})
23
- find(:first, scoped_options(options))
23
+ klass.first(scoped_options(options))
24
24
  end
25
25
 
26
26
  def last(options={})
27
- find(:last, scoped_options(options))
27
+ klass.last(scoped_options(options))
28
28
  end
29
29
 
30
- def count(conditions={})
31
- klass.count(conditions.deep_merge(scoped_conditions))
30
+ def count(options={})
31
+ klass.count(scoped_options(options))
32
32
  end
33
33
 
34
34
  def replace(docs)
@@ -57,20 +57,20 @@ module MongoMapper
57
57
  doc
58
58
  end
59
59
 
60
- def destroy_all(conditions={})
61
- all(:conditions => conditions).map(&:destroy)
60
+ def destroy_all(options={})
61
+ all(options).map(&:destroy)
62
62
  reset
63
63
  end
64
64
 
65
- def delete_all(conditions={})
66
- klass.delete_all(conditions.deep_merge(scoped_conditions))
65
+ def delete_all(options={})
66
+ klass.delete_all(options.merge(scoped_conditions))
67
67
  reset
68
68
  end
69
69
 
70
70
  def nullify
71
- criteria = FinderOptions.to_mongo_criteria(klass, scoped_conditions)
71
+ criteria = FinderOptions.new(klass, scoped_conditions).criteria
72
72
  all(criteria).each do |doc|
73
- doc.update_attributes self.foreign_key => nil
73
+ doc.update_attributes(self.foreign_key => nil)
74
74
  end
75
75
  reset
76
76
  end
@@ -89,13 +89,13 @@ module MongoMapper
89
89
  def scoped_conditions
90
90
  {self.foreign_key => @owner.id}
91
91
  end
92
-
92
+
93
93
  def scoped_options(options)
94
- options.deep_merge({:conditions => scoped_conditions})
94
+ @association.finder_options.merge(options).merge(scoped_conditions)
95
95
  end
96
96
 
97
97
  def find_target
98
- find(:all)
98
+ all
99
99
  end
100
100
 
101
101
  def ensure_owner_saved
@@ -1,6 +1,6 @@
1
1
  module MongoMapper
2
2
  module Associations
3
- class ManyEmbeddedPolymorphicProxy < Proxy
3
+ class ManyEmbeddedPolymorphicProxy < Proxy
4
4
  def replace(v)
5
5
  @_values = v.map do |doc_or_hash|
6
6
  if doc_or_hash.kind_of?(EmbeddedDocument)
@@ -30,4 +30,4 @@ module MongoMapper
30
30
  end
31
31
  end
32
32
  end
33
- end
33
+ end
@@ -1,6 +1,6 @@
1
1
  module MongoMapper
2
2
  module Associations
3
- class ManyPolymorphicProxy < ManyDocumentsProxy
3
+ class ManyPolymorphicProxy < ManyDocumentsProxy
4
4
  private
5
5
  def apply_scope(doc)
6
6
  doc.send("#{@association.type_key_name}=", doc.class.name)
@@ -6,6 +6,7 @@ module MongoMapper
6
6
  def initialize(owner, association)
7
7
  @owner = owner
8
8
  @association = association
9
+ @association.options[:extend].each { |ext| proxy_extend(ext) }
9
10
  reset
10
11
  end
11
12
 
@@ -1,4 +1,17 @@
1
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
2
15
  module Callbacks
3
16
  def self.included(model) #:nodoc:
4
17
  model.class_eval do
@@ -42,6 +55,11 @@ module MongoMapper
42
55
  return result
43
56
  end
44
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
45
63
  def destroy #:nodoc:
46
64
  return false if callback(:before_destroy) == false
47
65
  result = super
@@ -13,12 +13,12 @@ module MongoMapper
13
13
  extend Validations::Macros
14
14
  extend ClassMethods
15
15
  extend Finders
16
-
16
+
17
17
  def self.per_page
18
18
  25
19
19
  end unless respond_to?(:per_page)
20
20
  end
21
-
21
+
22
22
  descendants << model
23
23
  end
24
24
 
@@ -32,17 +32,29 @@ module MongoMapper
32
32
  create_indexes_for(key)
33
33
  key
34
34
  end
35
-
35
+
36
36
  def ensure_index(name_or_array, options={})
37
37
  keys_to_index = if name_or_array.is_a?(Array)
38
38
  name_or_array.map { |pair| [pair[0], pair[1]] }
39
39
  else
40
40
  name_or_array
41
41
  end
42
-
42
+
43
43
  MongoMapper.ensure_index(self, keys_to_index, options)
44
44
  end
45
-
45
+
46
+ # @overload find(:first, options)
47
+ # @see Document.first
48
+ #
49
+ # @overload find(:last, options)
50
+ # @see Document.last
51
+ #
52
+ # @overload find(:all, options)
53
+ # @see Document.all
54
+ #
55
+ # @overload find(ids, options)
56
+ #
57
+ # @raise DocumentNotFound raised when no ID or arguments are provided
46
58
  def find(*args)
47
59
  options = args.extract_options!
48
60
  case args.first
@@ -55,7 +67,7 @@ module MongoMapper
55
67
  when 0
56
68
  raise DocumentNotFound, "Couldn't find without an ID"
57
69
  when 1
58
- find_one(args[0], options)
70
+ find_one!(options.merge({:_id => args[0]}))
59
71
  else
60
72
  find_some(args, options)
61
73
  end
@@ -63,68 +75,113 @@ module MongoMapper
63
75
  end
64
76
 
65
77
  def paginate(options)
66
- per_page = options.delete(:per_page) || self.per_page
78
+ per_page = options.delete(:per_page) || self.per_page
67
79
  page = options.delete(:page)
68
- total_entries = count(options[:conditions] || {})
69
- collection = Pagination::PaginationProxy.new(total_entries, page, per_page)
80
+ total_entries = count(options)
81
+ pagination = Pagination::PaginationProxy.new(total_entries, page, per_page)
70
82
 
71
- options[:limit] = collection.limit
72
- options[:skip] = collection.skip
73
-
74
- collection.subject = find_every(options)
75
- collection
83
+ options.merge!(:limit => pagination.limit, :skip => pagination.skip)
84
+ pagination.subject = find_every(options)
85
+ pagination
76
86
  end
77
87
 
88
+ # @param [Hash] options any conditions understood by
89
+ # FinderOptions.to_mongo_criteria
90
+ #
91
+ # @return the first document in the ordered collection as described by
92
+ # +options+
93
+ #
94
+ # @see FinderOptions
78
95
  def first(options={})
79
- options.merge!(:limit => 1)
80
- find_every(options)[0]
96
+ find_one(options)
81
97
  end
82
98
 
99
+ # @param [Hash] options any conditions understood by
100
+ # FinderOptions.to_mongo_criteria
101
+ # @option [String] :order this *mandatory* option describes how to
102
+ # identify the ordering of the documents in your collection. Note that
103
+ # the *last* document in this collection will be selected.
104
+ #
105
+ # @return the last document in the ordered collection as described by
106
+ # +options+
107
+ #
108
+ # @raise Exception when no <tt>:order</tt> option has been defined
83
109
  def last(options={})
84
- if options[:order].blank?
85
- raise ':order option must be provided when using last'
86
- end
87
-
88
- options.merge!(:limit => 1)
89
- options[:order] = invert_order_clause(options[:order])
90
- find_every(options)[0]
110
+ raise ':order option must be provided when using last' if options[:order].blank?
111
+ find_one(options.merge(:order => invert_order_clause(options[:order])))
91
112
  end
92
113
 
114
+ # @param [Hash] options any conditions understood by
115
+ # FinderOptions.to_mongo_criteria
116
+ #
117
+ # @return [Array] all documents in your collection that match the
118
+ # provided conditions
119
+ #
120
+ # @see FinderOptions
93
121
  def all(options={})
94
122
  find_every(options)
95
123
  end
96
124
 
97
125
  def find_by_id(id)
98
- criteria = FinderOptions.to_mongo_criteria(self, :_id => id)
99
- if doc = collection.find_one(criteria)
100
- new(doc)
101
- end
126
+ find_one(:_id => id)
102
127
  end
103
128
 
104
- def count(conditions={})
105
- collection.find(FinderOptions.to_mongo_criteria(self, conditions)).count
129
+ def count(options={})
130
+ collection.find(to_criteria(options)).count
106
131
  end
107
132
 
108
- def exists?(conditions={})
109
- !count(conditions).zero?
133
+ def exists?(options={})
134
+ !count(options).zero?
110
135
  end
111
136
 
137
+ # @overload create(doc_attributes)
138
+ # Create a single new document
139
+ # @param [Hash] doc_attributes key/value pairs to create a new
140
+ # document
141
+ #
142
+ # @overload create(docs_attributes)
143
+ # Create many new documents
144
+ # @param [Array<Hash>] provide many Hashes of key/value pairs to create
145
+ # multiple documents
146
+ #
147
+ # @example Creating a single document
148
+ # MyModel.create({ :foo => "bar" })
149
+ #
150
+ # @example Creating multiple documents
151
+ # MyModel.create([{ :foo => "bar" }, { :foo => "baz" })
152
+ #
153
+ # @return [Boolean] when a document is successfully created, +true+ will
154
+ # be returned. If a document fails to create, +false+ will be returned.
112
155
  def create(*docs)
113
156
  initialize_each(*docs) { |doc| doc.save }
114
157
  end
115
158
 
159
+ # @see Document.create
160
+ #
161
+ # @raise [DocumentNotValid] raised if a document fails to create
116
162
  def create!(*docs)
117
163
  initialize_each(*docs) { |doc| doc.save! }
118
164
  end
119
165
 
120
- # For updating single document
166
+ # @overload update(id, attributes)
167
+ # Update a single document
168
+ # @param id the ID of the document you wish to update
169
+ # @param [Hash] attributes the key to update on the document with a new
170
+ # value
171
+ #
172
+ # @overload update(ids_and_attributes)
173
+ # Update multiple documents
174
+ # @param [Hash] ids_and_attributes each key is the ID of some document
175
+ # you wish to update. The value each key points toward are those
176
+ # applied to the target document
177
+ #
178
+ # @example Updating single document
121
179
  # Person.update(1, {:foo => 'bar'})
122
180
  #
123
- # For updating multiple documents at once:
181
+ # @example Updating multiple documents at once:
124
182
  # Person.update({'1' => {:foo => 'bar'}, '2' => {:baz => 'wick'}})
125
183
  def update(*args)
126
- updating_multiple = args.length == 1
127
- if updating_multiple
184
+ if args.length == 1
128
185
  update_multiple(args[0])
129
186
  else
130
187
  id, attributes = args
@@ -132,24 +189,55 @@ module MongoMapper
132
189
  end
133
190
  end
134
191
 
192
+ # Removes ("deletes") one or many documents from the collection. Note
193
+ # that this will bypass any +destroy+ hooks defined by your class.
194
+ #
195
+ # @param [Array] ids the ID or IDs of the records you wish to delete
135
196
  def delete(*ids)
136
- criteria = FinderOptions.to_mongo_criteria(self, :_id => ids.flatten)
137
- collection.remove(criteria)
197
+ collection.remove(to_criteria(:_id => ids.flatten))
138
198
  end
139
199
 
140
- def delete_all(conditions={})
141
- criteria = FinderOptions.to_mongo_criteria(self, conditions)
142
- collection.remove(criteria)
200
+ def delete_all(options={})
201
+ collection.remove(to_criteria(options))
143
202
  end
144
203
 
204
+ # Iterates over each document found by the provided IDs and calls their
205
+ # +destroy+ method. This has the advantage of processing your document's
206
+ # +destroy+ call-backs.
207
+ #
208
+ # @overload destroy(id)
209
+ # Destroy a single document by ID
210
+ # @param id the ID of the document to destroy
211
+ #
212
+ # @overload destroy(ids)
213
+ # Destroy many documents by their IDs
214
+ # @param [Array] the IDs of each document you wish to destroy
215
+ #
216
+ # @example Destroying a single document
217
+ # Person.destroy("34")
218
+ #
219
+ # @example Destroying multiple documents
220
+ # Person.destroy("34", "45", ..., "54")
221
+ #
222
+ # # OR...
223
+ #
224
+ # Person.destroy(["34", "45", ..., "54"])
145
225
  def destroy(*ids)
146
226
  find_some(ids.flatten).each(&:destroy)
147
227
  end
148
228
 
149
- def destroy_all(conditions={})
150
- all(conditions).each(&:destroy)
229
+ def destroy_all(options={})
230
+ all(options).each(&:destroy)
151
231
  end
152
232
 
233
+ # @overload connection()
234
+ # @return [Mongo::Connection] the connection used by your document class
235
+ #
236
+ # @overload connection(mongo_connection)
237
+ # @param [Mongo::Connection] mongo_connection a new connection for your
238
+ # document class to use
239
+ # @return [Mongo::Connection] a new Mongo::Connection for yoru document
240
+ # class
153
241
  def connection(mongo_connection=nil)
154
242
  if mongo_connection.nil?
155
243
  @connection ||= MongoMapper.connection
@@ -159,6 +247,12 @@ module MongoMapper
159
247
  @connection
160
248
  end
161
249
 
250
+ # @overload database()
251
+ # @return [Mongo] the database object used by your document class
252
+ #
253
+ # @overload database(name)
254
+ # @param [String] name the name of an existing, or new, Mongo database
255
+ # @return [Mongo] a Mongo database object for the specified database
162
256
  def database(name=nil)
163
257
  if name.nil?
164
258
  @database ||= MongoMapper.database
@@ -167,42 +261,49 @@ module MongoMapper
167
261
  end
168
262
  @database
169
263
  end
170
-
264
+
171
265
  # Changes the collection name from the default to whatever you want
266
+ #
267
+ # @param [#to_s] name the new collection name to use. Defaults to +nil+
172
268
  def set_collection_name(name=nil)
173
269
  @collection = nil
174
270
  @collection_name = name
175
271
  end
176
-
272
+
177
273
  # Returns the collection name, if not set, defaults to class name tableized
274
+ #
275
+ # @return [String] the collection name, if not set, defaults to class
276
+ # name tableized
178
277
  def collection_name
179
- @collection_name ||= self.to_s.demodulize.tableize
278
+ @collection_name ||= self.to_s.tableize.gsub(/\//, '.')
180
279
  end
181
280
 
182
- # Returns the mongo ruby driver collection object
281
+ # @return the Mongo Ruby driver +collection+ object
183
282
  def collection
184
283
  @collection ||= database.collection(collection_name)
185
284
  end
186
285
 
286
+ # Defines a +created_at+ and +updated_at+ attribute (with a +Time+
287
+ # value) on your document. These attributes are updated by an
288
+ # injected +update_timestamps+ +before_save+ hook.
187
289
  def timestamps!
188
290
  key :created_at, Time
189
291
  key :updated_at, Time
190
-
191
292
  class_eval { before_save :update_timestamps }
192
293
  end
193
-
294
+
194
295
  def single_collection_inherited?
195
296
  keys.has_key?('_type') && single_collection_inherited_superclass?
196
297
  end
197
-
298
+
198
299
  def single_collection_inherited_superclass?
199
300
  superclass.respond_to?(:keys) && superclass.keys.has_key?('_type')
200
301
  end
201
-
302
+
202
303
  protected
203
304
  def method_missing(method, *args)
204
305
  finder = DynamicFinder.new(method)
205
-
306
+
206
307
  if finder.found?
207
308
  meta_def(finder.method) { |*args| dynamic_find(finder, args) }
208
309
  send(finder.method, *args)
@@ -212,50 +313,41 @@ module MongoMapper
212
313
  end
213
314
 
214
315
  private
215
- # Initializes each document and yields each initialized document
316
+ def create_indexes_for(key)
317
+ ensure_index key.name if key.options[:index]
318
+ end
319
+
216
320
  def initialize_each(*docs)
217
321
  instances = []
218
322
  docs = [{}] if docs.blank?
219
323
  docs.flatten.each do |attrs|
220
- doc = new(attrs)
324
+ doc = initialize_doc(attrs)
221
325
  yield(doc)
222
326
  instances << doc
223
327
  end
224
328
  instances.size == 1 ? instances[0] : instances
225
329
  end
226
330
 
227
- def create_indexes_for(key)
228
- ensure_index key.name if key.options[:index]
331
+ def initialize_doc(doc)
332
+ begin
333
+ klass = doc['_type'].present? ? doc['_type'].constantize : self
334
+ klass.new(doc)
335
+ rescue NameError
336
+ new(doc)
337
+ end
229
338
  end
230
-
339
+
231
340
  def find_every(options)
232
- criteria, options = FinderOptions.new(self, options).to_a
341
+ criteria, options = to_finder_options(options)
233
342
  collection.find(criteria, options).to_a.map do |doc|
234
- begin
235
- klass = doc['_type'].present? ? doc['_type'].constantize : self
236
- klass.new(doc)
237
- rescue NameError
238
- new(doc)
239
- end
343
+ initialize_doc(doc)
240
344
  end
241
345
  end
242
346
 
243
- def invert_order_clause(order)
244
- order.split(',').map do |order_segment|
245
- if order_segment =~ /\sasc/i
246
- order_segment.sub /\sasc/i, ' desc'
247
- elsif order_segment =~ /\sdesc/i
248
- order_segment.sub /\sdesc/i, ' asc'
249
- else
250
- "#{order_segment.strip} desc"
251
- end
252
- end.join(',')
253
- end
254
-
255
347
  def find_some(ids, options={})
256
- ids = ids.flatten.compact.uniq
257
- documents = find_every(options.deep_merge(:conditions => {'_id' => ids}))
258
-
348
+ ids = ids.flatten.compact.uniq
349
+ documents = find_every(options.merge(:_id => ids))
350
+
259
351
  if ids.size == documents.size
260
352
  documents
261
353
  else
@@ -263,14 +355,29 @@ module MongoMapper
263
355
  end
264
356
  end
265
357
 
266
- def find_one(id, options={})
267
- if doc = find_every(options.deep_merge(:conditions => {:_id => id})).first
268
- doc
269
- else
270
- raise DocumentNotFound, "Document with id of #{id} does not exist in collection named #{collection.name}"
358
+ def find_one(options={})
359
+ criteria, options = to_finder_options(options)
360
+ if doc = collection.find_one(criteria, options)
361
+ initialize_doc(doc)
271
362
  end
272
363
  end
273
364
 
365
+ def find_one!(options={})
366
+ find_one(options) || raise(DocumentNotFound, "Document match #{options.inspect} does not exist in #{collection.name} collection")
367
+ end
368
+
369
+ def invert_order_clause(order)
370
+ order.split(',').map do |order_segment|
371
+ if order_segment =~ /\sasc/i
372
+ order_segment.sub /\sasc/i, ' desc'
373
+ elsif order_segment =~ /\sdesc/i
374
+ order_segment.sub /\sdesc/i, ' asc'
375
+ else
376
+ "#{order_segment.strip} desc"
377
+ end
378
+ end.join(',')
379
+ end
380
+
274
381
  def update_single(id, attrs)
275
382
  if id.blank? || attrs.blank? || !attrs.is_a?(Hash)
276
383
  raise ArgumentError, "Updating a single document requires an id and a hash of attributes"
@@ -290,9 +397,17 @@ module MongoMapper
290
397
  docs.each_pair { |id, attrs| instances << update(id, attrs) }
291
398
  instances
292
399
  end
400
+
401
+ def to_criteria(options={})
402
+ FinderOptions.new(self, options).criteria
403
+ end
404
+
405
+ def to_finder_options(options={})
406
+ FinderOptions.new(self, options).to_a
407
+ end
293
408
  end
294
409
 
295
- module InstanceMethods
410
+ module InstanceMethods
296
411
  def collection
297
412
  self.class.collection
298
413
  end
@@ -311,12 +426,14 @@ module MongoMapper
311
426
 
312
427
  def destroy
313
428
  return false if frozen?
314
-
315
- criteria = FinderOptions.to_mongo_criteria(self.class, :_id => id)
316
- collection.remove(criteria) unless new?
429
+ self.class.delete(id) unless new?
317
430
  freeze
318
431
  end
319
432
 
433
+ def reload
434
+ self.class.find(id)
435
+ end
436
+
320
437
  private
321
438
  def create_or_update
322
439
  result = new? ? create : update
@@ -327,7 +444,7 @@ module MongoMapper
327
444
  assign_id
328
445
  save_to_collection
329
446
  end
330
-
447
+
331
448
  def assign_id
332
449
  if read_attribute(:_id).blank?
333
450
  write_attribute(:_id, Mongo::ObjectID.new.to_s)
@@ -348,7 +465,7 @@ module MongoMapper
348
465
  write_attribute('created_at', now) if new?
349
466
  write_attribute('updated_at', now)
350
467
  end
351
-
468
+
352
469
  def clear_custom_id_flag
353
470
  @using_custom_id = nil
354
471
  end