mongodoc 0.1.2 → 0.2.0

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