mongodoc 0.1.2 → 0.2.0

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 (47) hide show
  1. data/README.textile +143 -0
  2. data/Rakefile +35 -3
  3. data/VERSION +1 -1
  4. data/examples/simple_document.rb +35 -0
  5. data/examples/simple_object.rb +32 -0
  6. data/features/finders.feature +72 -0
  7. data/features/mongodoc_base.feature +12 -2
  8. data/features/named_scopes.feature +66 -0
  9. data/features/new_record.feature +36 -0
  10. data/features/partial_updates.feature +105 -0
  11. data/features/step_definitions/criteria_steps.rb +4 -41
  12. data/features/step_definitions/document_steps.rb +56 -5
  13. data/features/step_definitions/documents.rb +14 -3
  14. data/features/step_definitions/finder_steps.rb +15 -0
  15. data/features/step_definitions/named_scope_steps.rb +18 -0
  16. data/features/step_definitions/partial_update_steps.rb +32 -0
  17. data/features/step_definitions/query_steps.rb +51 -0
  18. data/features/using_criteria.feature +5 -1
  19. data/lib/mongodoc/attributes.rb +76 -63
  20. data/lib/mongodoc/collection.rb +9 -9
  21. data/lib/mongodoc/criteria.rb +152 -161
  22. data/lib/mongodoc/cursor.rb +7 -5
  23. data/lib/mongodoc/document.rb +95 -31
  24. data/lib/mongodoc/finders.rb +29 -0
  25. data/lib/mongodoc/named_scope.rb +68 -0
  26. data/lib/mongodoc/parent_proxy.rb +15 -6
  27. data/lib/mongodoc/proxy.rb +22 -13
  28. data/lib/mongodoc.rb +3 -3
  29. data/mongodoc.gemspec +42 -14
  30. data/perf/mongodoc_runner.rb +90 -0
  31. data/perf/ruby_driver_runner.rb +64 -0
  32. data/spec/attributes_spec.rb +46 -12
  33. data/spec/collection_spec.rb +23 -23
  34. data/spec/criteria_spec.rb +124 -187
  35. data/spec/cursor_spec.rb +21 -17
  36. data/spec/document_ext.rb +2 -2
  37. data/spec/document_spec.rb +187 -218
  38. data/spec/embedded_save_spec.rb +104 -0
  39. data/spec/finders_spec.rb +81 -0
  40. data/spec/hash_matchers.rb +27 -0
  41. data/spec/named_scope_spec.rb +82 -0
  42. data/spec/new_record_spec.rb +216 -0
  43. data/spec/parent_proxy_spec.rb +8 -6
  44. data/spec/proxy_spec.rb +80 -0
  45. data/spec/spec_helper.rb +2 -0
  46. metadata +35 -7
  47. data/README.rdoc +0 -75
@@ -12,7 +12,7 @@ module MongoDoc #:nodoc:
12
12
  #
13
13
  # <tt>criteria = Criteria.new</tt>
14
14
  #
15
- # <tt>criteria.select(:field => "value").only(:field).skip(20).limit(20)</tt>
15
+ # <tt>criteria.only(:field => "value").only(:field).skip(20).limit(20)</tt>
16
16
  #
17
17
  # <tt>criteria.execute</tt>
18
18
  class Criteria
@@ -25,7 +25,17 @@ module MongoDoc #:nodoc:
25
25
 
26
26
  include Enumerable
27
27
 
28
- attr_reader :klass, :options, :selector
28
+ attr_reader :collection, :klass, :options, :selector
29
+
30
+ # Create the new +Criteria+ object. This will initialize the selector
31
+ # and options hashes, as well as the type of criteria.
32
+ #
33
+ # Options:
34
+ #
35
+ # klass: The class to execute on.
36
+ def initialize(klass)
37
+ @selector, @options, @klass = {}, {}, klass
38
+ end
29
39
 
30
40
  # Returns true if the supplied +Enumerable+ or +Criteria+ is equal to the results
31
41
  # of this +Criteria+ or the criteria itself.
@@ -41,7 +51,7 @@ module MongoDoc #:nodoc:
41
51
  self.selector == other.selector && self.options == other.options
42
52
  when Enumerable
43
53
  @collection ||= execute
44
- return (@collection == other)
54
+ return (collection == other)
45
55
  else
46
56
  return false
47
57
  end
@@ -55,31 +65,20 @@ module MongoDoc #:nodoc:
55
65
  #
56
66
  # Example:
57
67
  #
58
- # <tt>criteria.select(:field1).where(:field1 => "Title").aggregate(Person)</tt>
59
- def aggregate(use_klass = nil)
60
- aggregating_klass = use_klass ? use_klass : klass
61
- aggregating_klass.collection.group(options[:fields], selector, { :count => 0 }, AGGREGATE_REDUCE)
68
+ # <tt>criteria.only(:field1).where(:field1 => "Title").aggregate</tt>
69
+ def aggregate
70
+ klass.collection.group(options[:fields], selector, { :count => 0 }, AGGREGATE_REDUCE, true)
62
71
  end
63
72
 
64
- # Adds a criterion to the +Criteria+ that specifies values that must all
65
- # be matched in order to return results. Similar to an "in" clause but the
66
- # underlying conditional logic is an "AND" and not an "OR". The MongoDB
67
- # conditional operator that will be used is "$all".
68
- #
69
- # Options:
70
- #
71
- # selections: A +Hash+ where the key is the field name and the value is an
72
- # +Array+ of values that must all match.
73
+ # Get all the matching documents in the database for the +Criteria+.
73
74
  #
74
75
  # Example:
75
76
  #
76
- # <tt>criteria.every(:field => ["value1", "value2"])</tt>
77
+ # <tt>criteria.all</tt>
77
78
  #
78
- # <tt>criteria.every(:field1 => ["value1", "value2"], :field2 => ["value1"])</tt>
79
- #
80
- # Returns: <tt>self</tt>
81
- def every(selections = {})
82
- selections.each { |key, value| selector[key] = { "$all" => value } }; self
79
+ # Returns: <tt>Array</tt>
80
+ def all
81
+ collect
83
82
  end
84
83
 
85
84
  # Get the count of matching documents in the database for the +Criteria+.
@@ -102,12 +101,97 @@ module MongoDoc #:nodoc:
102
101
  def each(&block)
103
102
  @collection ||= execute
104
103
  if block_given?
105
- @collection.each(&block)
106
- else
107
- self
104
+ @collection = collection.inject([]) do |container, item|
105
+ container << item
106
+ yield item
107
+ container
108
+ end
109
+ end
110
+ self
111
+ end
112
+
113
+ GROUP_REDUCE = "function(obj, prev) { prev.group.push(obj); }"
114
+ # Groups the criteria. This will take the internally built selector and options
115
+ # and pass them on to the Ruby driver's +group()+ method on the collection. The
116
+ # collection itself will be retrieved from the class provided, and once the
117
+ # query has returned it will provided a grouping of keys with objects.
118
+ #
119
+ # Example:
120
+ #
121
+ # <tt>criteria.only(:field1).where(:field1 => "Title").group</tt>
122
+ def group
123
+ klass.collection.group(
124
+ options[:fields],
125
+ selector,
126
+ { :group => [] },
127
+ GROUP_REDUCE,
128
+ true
129
+ ).collect {|docs| docs["group"] = MongoDoc::BSON.decode(docs["group"]); docs }
130
+ end
131
+
132
+ # Return the last result for the +Criteria+. Essentially does a find_one on
133
+ # the collection with the sorting reversed. If no sorting parameters have
134
+ # been provided it will default to ids.
135
+ #
136
+ # Example:
137
+ #
138
+ # <tt>Criteria.only(:name).where(:name = "Chrissy").last</tt>
139
+ def last
140
+ opts = options.dup
141
+ sorting = opts[:sort]
142
+ sorting = [[:_id, :asc]] unless sorting
143
+ opts[:sort] = sorting.collect { |option| [ option.first, Criteria.invert(option.last) ] }
144
+ klass.collection.find_one(selector, opts)
145
+ end
146
+
147
+ # Return the first result for the +Criteria+.
148
+ #
149
+ # Example:
150
+ #
151
+ # <tt>Criteria.only(:name).where(:name = "Chrissy").one</tt>
152
+ def one
153
+ klass.collection.find_one(selector, options.dup)
154
+ end
155
+ alias :first :one
156
+
157
+ # Translate the supplied argument hash
158
+ #
159
+ # Options:
160
+ #
161
+ # criteria_conditions: Hash of criteria keys, and parameter values
162
+ #
163
+ # Example:
164
+ #
165
+ # <tt>criteria.criteria(:where => { :field => "value"}, :limit => 20)</tt>
166
+ #
167
+ # Returns <tt>self</tt>
168
+ def criteria(criteria_conditions = {})
169
+ criteria_conditions.inject(self) do |criteria, (key, value)|
170
+ criteria.send(key, value)
108
171
  end
109
172
  end
110
173
 
174
+ # Adds a criterion to the +Criteria+ that specifies values that must all
175
+ # be matched in order to return results. Similar to an "in" clause but the
176
+ # underlying conditional logic is an "AND" and not an "OR". The MongoDB
177
+ # conditional operator that will be used is "$all".
178
+ #
179
+ # Options:
180
+ #
181
+ # selections: A +Hash+ where the key is the field name and the value is an
182
+ # +Array+ of values that must all match.
183
+ #
184
+ # Example:
185
+ #
186
+ # <tt>criteria.every(:field => ["value1", "value2"])</tt>
187
+ #
188
+ # <tt>criteria.every(:field1 => ["value1", "value2"], :field2 => ["value1"])</tt>
189
+ #
190
+ # Returns: <tt>self</tt>
191
+ def every(selections = {})
192
+ selections.each { |key, value| selector[key] = { "$all" => value } }; self
193
+ end
194
+
111
195
  # Adds a criterion to the +Criteria+ that specifies values that are not allowed
112
196
  # to match any document in the database. The MongoDB conditional operator that
113
197
  # will be used is "$ne".
@@ -146,32 +230,19 @@ module MongoDoc #:nodoc:
146
230
  self
147
231
  end
148
232
 
149
- # Return the first result for the +Criteria+.
233
+ # Adds a criterion to the +Criteria+ that specifies an id that must be matched.
150
234
  #
151
- # Example:
235
+ # Options:
152
236
  #
153
- # <tt>Criteria.select(:name).where(:name = "Chrissy").one</tt>
154
- def one
155
- klass.collection.find_one(selector, options.dup)
156
- end
157
- alias :first :one
158
-
159
- GROUP_REDUCE = "function(obj, prev) { prev.group.push(obj); }"
160
- # Groups the criteria. This will take the internally built selector and options
161
- # and pass them on to the Ruby driver's +group()+ method on the collection. The
162
- # collection itself will be retrieved from the class provided, and once the
163
- # query has returned it will provided a grouping of keys with objects.
237
+ # id_or_object_id: A +String+ representation of a <tt>Mongo::ObjectID</tt>
164
238
  #
165
239
  # Example:
166
240
  #
167
- # <tt>criteria.select(:field1).where(:field1 => "Title").group(Person)</tt>
168
- def group(use_klass = nil)
169
- (use_klass || klass).collection.group(
170
- options[:fields],
171
- selector,
172
- { :group => [] },
173
- GROUP_REDUCE
174
- ).collect {|docs| docs["group"] = MongoDoc::BSON.decode(docs["group"]); docs }
241
+ # <tt>criteria.id("4ab2bc4b8ad548971900005c")</tt>
242
+ #
243
+ # Returns: <tt>self</tt>
244
+ def id(id_or_object_id)
245
+ selector[:_id] = id_or_object_id; self
175
246
  end
176
247
 
177
248
  # Adds a criterion to the +Criteria+ that specifies values where any can
@@ -194,47 +265,6 @@ module MongoDoc #:nodoc:
194
265
  inclusions.each { |key, value| selector[key] = { "$in" => value } }; self
195
266
  end
196
267
 
197
- # Adds a criterion to the +Criteria+ that specifies an id that must be matched.
198
- #
199
- # Options:
200
- #
201
- # object_id: A +String+ representation of a <tt>Mongo::ObjectID</tt>
202
- #
203
- # Example:
204
- #
205
- # <tt>criteria.id("4ab2bc4b8ad548971900005c")</tt>
206
- #
207
- # Returns: <tt>self</tt>
208
- def id(object_id)
209
- selector[:_id] = object_id; self
210
- end
211
-
212
- # Create the new +Criteria+ object. This will initialize the selector
213
- # and options hashes, as well as the type of criteria.
214
- #
215
- # Options:
216
- #
217
- # type: One of :all, :first:, or :last
218
- # klass: The class to execute on.
219
- def initialize(klass)
220
- @selector, @options, @klass = {}, {}, klass
221
- end
222
-
223
- # Return the last result for the +Criteria+. Essentially does a find_one on
224
- # the collection with the sorting reversed. If no sorting parameters have
225
- # been provided it will default to ids.
226
- #
227
- # Example:
228
- #
229
- # <tt>Criteria.select(:name).where(:name = "Chrissy").last</tt>
230
- def last
231
- opts = options.dup
232
- sorting = opts[:sort]
233
- sorting = [[:_id, :asc]] unless sorting
234
- opts[:sort] = sorting.collect { |option| [ option.first, Criteria.invert(option.last) ] }
235
- klass.collection.find_one(selector, opts)
236
- end
237
-
238
268
  # Adds a criterion to the +Criteria+ that specifies the maximum number of
239
269
  # results to return. This is mostly used in conjunction with <tt>skip()</tt>
240
270
  # to handle paginated results.
@@ -252,56 +282,6 @@ module MongoDoc #:nodoc:
252
282
  options[:limit] = value; self
253
283
  end
254
284
 
255
- # Merges another object into this +Criteria+. The other object may be a
256
- # +Criteria+ or a +Hash+. This is used to combine multiple scopes together,
257
- # where a chained scope situation may be desired.
258
- #
259
- # Options:
260
- #
261
- # other: The +Criteria+ or +Hash+ to merge with.
262
- #
263
- # Example:
264
- #
265
- # <tt>criteria.merge({ :conditions => { :title => "Sir" } })</tt>
266
- def merge(other)
267
- selector.update(other.selector)
268
- options.update(other.options)
269
- end
270
-
271
- # Used for chaining +Criteria+ scopes together in the for of class methods
272
- # on the +Document+ the criteria is for.
273
- #
274
- # Options:
275
- #
276
- # name: The name of the class method on the +Document+ to chain.
277
- # args: The arguments passed to the method.
278
- #
279
- # Example:
280
- #
281
- # class Person < Mongoid::Document
282
- # field :title
283
- # field :terms, :type => Boolean, :default => false
284
- #
285
- # class << self
286
- # def knights
287
- # all(:conditions => { :title => "Sir" })
288
- # end
289
- #
290
- # def accepted
291
- # all(:conditions => { :terms => true })
292
- # end
293
- # end
294
- # end
295
- #
296
- # Person.accepted.knights #returns a merged criteria of the 2 scopes.
297
- #
298
- # Returns: <tt>Criteria</tt>
299
- def method_missing(name, *args)
300
- new_scope = klass.send(name)
301
- new_scope.merge(self)
302
- new_scope
303
- end
304
-
305
285
  # Adds a criterion to the +Criteria+ that specifies values where none
306
286
  # should match in order to return results. This is similar to an SQL "NOT IN"
307
287
  # clause. The MongoDB conditional operator that will be used is "$nin".
@@ -363,7 +343,7 @@ module MongoDoc #:nodoc:
363
343
  def paginate
364
344
  @collection ||= execute
365
345
  WillPaginate::Collection.create(page, per_page, count) do |pager|
366
- pager.replace(@collection.to_a)
346
+ pager.replace(collection.to_a)
367
347
  end
368
348
  end
369
349
 
@@ -382,10 +362,10 @@ module MongoDoc #:nodoc:
382
362
  #
383
363
  # Example:
384
364
  #
385
- # <tt>criteria.select(:field1, :field2, :field3)</tt>
365
+ # <tt>criteria.only(:field1, :field2, :field3)</tt>
386
366
  #
387
367
  # Returns: <tt>self</tt>
388
- def select(*args)
368
+ def only(*args)
389
369
  options[:fields] = args.flatten if args.any?; self
390
370
  end
391
371
 
@@ -407,6 +387,36 @@ module MongoDoc #:nodoc:
407
387
  options[:skip] = value; self
408
388
  end
409
389
 
390
+ # Adds a criterion to the +Criteria+ that specifies values that must
391
+ # be matched in order to return results. This is similar to a SQL "WHERE"
392
+ # clause. This is the actual selector that will be provided to MongoDB,
393
+ # similar to the Javascript object that is used when performing a find()
394
+ # in the MongoDB console.
395
+ #
396
+ # Options:
397
+ #
398
+ # selector_or_js: A +Hash+ that must match the attributes of the +Document+
399
+ # or a +String+ of js code.
400
+ #
401
+ # Example:
402
+ #
403
+ # <tt>criteria.where(:field1 => "value1", :field2 => 15)</tt>
404
+ #
405
+ # <tt>criteria.where('this.a > 3')</tt>
406
+ #
407
+ # Returns: <tt>self</tt>
408
+ def where(selector_or_js = {})
409
+ case selector_or_js
410
+ when String
411
+ selector['$where'] = selector_or_js
412
+ else
413
+ selector.merge!(selector_or_js)
414
+ end
415
+ self
416
+ end
417
+ alias :and :where
418
+ alias :conditions :where
419
+
410
420
  # Translate the supplied arguments into a +Criteria+ object.
411
421
  #
412
422
  # If the passed in args is a single +String+, then it will
@@ -427,27 +437,8 @@ module MongoDoc #:nodoc:
427
437
  #
428
438
  # Returns a new +Criteria+ object.
429
439
  def self.translate(klass, params = {})
430
- return new(klass).id(params).one if params.is_a?(String)
431
- return new(klass).where(params.delete(:conditions)).extras(params)
432
- end
433
-
434
- # Adds a criterion to the +Criteria+ that specifies values that must
435
- # be matched in order to return results. This is similar to a SQL "WHERE"
436
- # clause. This is the actual selector that will be provided to MongoDB,
437
- # similar to the Javascript object that is used when performing a find()
438
- # in the MongoDB console.
439
- #
440
- # Options:
441
- #
442
- # selectior: A +Hash+ that must match the attributes of the +Document+.
443
- #
444
- # Example:
445
- #
446
- # <tt>criteria.where(:field1 => "value1", :field2 => 15)</tt>
447
- #
448
- # Returns: <tt>self</tt>
449
- def where(add_selector = {})
450
- selector.merge!(add_selector); self
440
+ return new(klass).id(params).one unless params.is_a?(Hash)
441
+ return new(klass).criteria(params)
451
442
  end
452
443
 
453
444
  protected
@@ -1,5 +1,7 @@
1
1
  module MongoDoc
2
2
  class Cursor
3
+ include Enumerable
4
+
3
5
  attr_accessor :_cursor
4
6
  delegate :close, :closed?, :count, :explain, :limit, :query_options_hash, :query_opts, :skip, :sort, :to => :_cursor
5
7
 
@@ -8,17 +10,17 @@ module MongoDoc
8
10
  end
9
11
 
10
12
  def each
11
- _cursor.each do |next_object|
12
- yield MongoDoc::BSON.decode(next_object)
13
+ _cursor.each do |next_document|
14
+ yield MongoDoc::BSON.decode(next_document)
13
15
  end
14
16
  end
15
17
 
16
- def next_object
17
- MongoDoc::BSON.decode(_cursor.next_object)
18
+ def next_document
19
+ MongoDoc::BSON.decode(_cursor.next_document)
18
20
  end
19
21
 
20
22
  def to_a
21
23
  MongoDoc::BSON.decode(_cursor.to_a)
22
24
  end
23
25
  end
24
- end
26
+ end
@@ -2,18 +2,27 @@ require 'mongodoc/bson'
2
2
  require 'mongodoc/query'
3
3
  require 'mongodoc/attributes'
4
4
  require 'mongodoc/criteria'
5
+ require 'mongodoc/finders'
6
+ require 'mongodoc/named_scope'
5
7
 
6
8
  module MongoDoc
7
9
  class DocumentInvalidError < RuntimeError; end
8
10
  class NotADocumentError < RuntimeError; end
9
11
 
10
- class Document
11
- extend MongoDoc::Attributes
12
- include Validatable
12
+ module Document
13
13
 
14
- attr_accessor :_id
15
- alias :id :_id
16
- alias :to_param :_id
14
+ def self.included(klass)
15
+ klass.class_eval do
16
+ include Attributes
17
+ extend ClassMethods
18
+ extend Finders
19
+ extend NamedScope
20
+ include Validatable
21
+
22
+ alias :id :_id
23
+ alias :to_param :_id
24
+ end
25
+ end
17
26
 
18
27
  def initialize(attrs = {})
19
28
  self.attributes = attrs
@@ -24,6 +33,13 @@ module MongoDoc
24
33
  self.class._attributes.all? {|var| self.send(var) == other.send(var)}
25
34
  end
26
35
 
36
+ def attributes
37
+ self.class._attributes.inject({}) do |hash, attr|
38
+ hash[attr] = send(attr)
39
+ hash
40
+ end
41
+ end
42
+
27
43
  def attributes=(attrs)
28
44
  attrs.each do |key, value|
29
45
  send("#{key}=", value)
@@ -39,7 +55,7 @@ module MongoDoc
39
55
  return _save(false) unless validate and not valid?
40
56
  false
41
57
  end
42
-
58
+
43
59
  def save!
44
60
  return _root.save! if _root
45
61
  raise DocumentInvalidError unless valid?
@@ -56,18 +72,28 @@ module MongoDoc
56
72
  end
57
73
 
58
74
  def update_attributes(attrs)
75
+ strict = attrs.delete(:__strict__)
59
76
  self.attributes = attrs
60
- return _propose_update_attributes(self, path_to_root(attrs), false) if valid?
61
- false
77
+ return false unless valid?
78
+ if strict
79
+ _strict_update_attributes(_path_to_root(self, attrs), false)
80
+ else
81
+ _naive_update_attributes(_path_to_root(self, attrs), false)
82
+ end
62
83
  end
63
84
 
64
85
  def update_attributes!(attrs)
86
+ strict = attrs.delete(:__strict__)
65
87
  self.attributes = attrs
66
88
  raise DocumentInvalidError unless valid?
67
- _propose_update_attributes(self, path_to_root(attrs), true)
89
+ if strict
90
+ _strict_update_attributes(_path_to_root(self, attrs), true)
91
+ else
92
+ _naive_update_attributes(_path_to_root(self, attrs), true)
93
+ end
68
94
  end
69
95
 
70
- class << self
96
+ module ClassMethods
71
97
  def bson_create(bson_hash, options = {})
72
98
  new.tap do |obj|
73
99
  bson_hash.each do |name, value|
@@ -84,16 +110,12 @@ module MongoDoc
84
110
  self.to_s.tableize.gsub('/', '.')
85
111
  end
86
112
 
87
- def count
88
- collection.count
89
- end
90
-
91
113
  def create(attrs = {})
92
114
  instance = new(attrs)
93
115
  _create(instance, false) if instance.valid?
94
116
  instance
95
117
  end
96
-
118
+
97
119
  def create!(attrs = {})
98
120
  instance = new(attrs)
99
121
  raise MongoDoc::DocumentInvalidError unless instance.valid?
@@ -101,12 +123,16 @@ module MongoDoc
101
123
  instance
102
124
  end
103
125
 
104
- def criteria
105
- Criteria.new(self)
106
- end
126
+ protected
107
127
 
108
- def find_one(id)
109
- MongoDoc::BSON.decode(collection.find_one(id))
128
+ def _create(instance, safe)
129
+ instance.send(:notify_before_save_observers)
130
+ instance._id = collection.insert(instance, :safe => safe)
131
+ instance.send(:notify_save_success_observers)
132
+ instance._id
133
+ rescue Mongo::MongoDBError => e
134
+ instance.send(:notify_save_failed_observers)
135
+ raise e
110
136
  end
111
137
  end
112
138
 
@@ -115,24 +141,62 @@ module MongoDoc
115
141
  def _collection
116
142
  self.class.collection
117
143
  end
118
-
119
- def _propose_update_attributes(src, attrs, safe)
120
- return _parent.send(:_propose_update_attributes, src, attrs, safe) if _parent
121
- _update_attributes(attrs, safe)
144
+
145
+ def _naive_update_attributes(attrs, safe)
146
+ return _root.send(:_naive_update_attributes, attrs, safe) if _root
147
+ _collection.update({'_id' => self._id}, MongoDoc::Query.set_modifier(attrs), :safe => safe)
148
+ end
149
+
150
+ def _strict_update_attributes(attrs, safe, selector = {})
151
+ return _root.send(:_strict_update_attributes, attrs, safe, _selector_path_to_root('_id' => _id)) if _root
152
+ _collection.update({'_id' => _id}.merge(selector), MongoDoc::Query.set_modifier(attrs), :safe => safe)
122
153
  end
123
154
 
124
155
  def _save(safe)
156
+ notify_before_save_observers
125
157
  self._id = _collection.save(self, :safe => safe)
158
+ notify_save_success_observers
159
+ self._id
160
+ rescue Mongo::MongoDBError => e
161
+ notify_save_failed_observers
162
+ raise e
126
163
  end
127
164
 
128
- def _update_attributes(attrs, safe)
129
- _collection.update({'_id' => self._id}, MongoDoc::Query.set_modifier(attrs), :safe => safe)
165
+ def before_save_callback(root)
166
+ self._id = Mongo::ObjectID.new if new_record?
130
167
  end
131
168
 
132
- class << self
133
- def _create(instance, safe)
134
- instance._id = collection.insert(instance, :safe => safe)
135
- end
169
+ def save_failed_callback(root)
170
+ self._id = nil
171
+ end
172
+
173
+ def save_success_callback(root)
174
+ root.unregister_save_observer(self)
175
+ end
176
+
177
+ def save_observers
178
+ @save_observers ||= []
179
+ end
180
+
181
+ def register_save_observer(child)
182
+ save_observers << child
136
183
  end
184
+
185
+ def unregister_save_observer(child)
186
+ save_observers.delete(child)
187
+ end
188
+
189
+ def notify_before_save_observers
190
+ save_observers.each {|obs| obs.before_save_callback(self) }
191
+ end
192
+
193
+ def notify_save_success_observers
194
+ save_observers.each {|obs| obs.save_success_callback(self) }
195
+ end
196
+
197
+ def notify_save_failed_observers
198
+ save_observers.each {|obs| obs.save_failed_callback(self) }
199
+ end
200
+
137
201
  end
138
202
  end
@@ -0,0 +1,29 @@
1
+ module MongoDoc
2
+ module Finders
3
+ [:all, :count, :first, :last].each do |name|
4
+ module_eval <<-RUBY
5
+ def #{name}
6
+ Criteria.new(self).#{name}
7
+ end
8
+ RUBY
9
+ end
10
+
11
+ def criteria
12
+ Criteria.new(self)
13
+ end
14
+
15
+ def find(*args)
16
+ query = args.extract_options!
17
+ which = args.first
18
+ Criteria.translate(self, query).send(which)
19
+ end
20
+
21
+ def find_one(conditions_or_id)
22
+ if Hash === conditions_or_id
23
+ Criteria.translate(self, conditions_or_id).one
24
+ else
25
+ Criteria.translate(self, conditions_or_id)
26
+ end
27
+ end
28
+ end
29
+ end