mrkurt-mongo_mapper 0.6.8

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 (77) hide show
  1. data/.gitignore +10 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +38 -0
  4. data/Rakefile +55 -0
  5. data/VERSION +1 -0
  6. data/bin/mmconsole +60 -0
  7. data/lib/mongo_mapper.rb +139 -0
  8. data/lib/mongo_mapper/associations.rb +72 -0
  9. data/lib/mongo_mapper/associations/base.rb +113 -0
  10. data/lib/mongo_mapper/associations/belongs_to_polymorphic_proxy.rb +26 -0
  11. data/lib/mongo_mapper/associations/belongs_to_proxy.rb +21 -0
  12. data/lib/mongo_mapper/associations/collection.rb +19 -0
  13. data/lib/mongo_mapper/associations/many_documents_as_proxy.rb +26 -0
  14. data/lib/mongo_mapper/associations/many_documents_proxy.rb +115 -0
  15. data/lib/mongo_mapper/associations/many_embedded_polymorphic_proxy.rb +31 -0
  16. data/lib/mongo_mapper/associations/many_embedded_proxy.rb +54 -0
  17. data/lib/mongo_mapper/associations/many_polymorphic_proxy.rb +11 -0
  18. data/lib/mongo_mapper/associations/one_proxy.rb +61 -0
  19. data/lib/mongo_mapper/associations/proxy.rb +111 -0
  20. data/lib/mongo_mapper/callbacks.rb +61 -0
  21. data/lib/mongo_mapper/dirty.rb +117 -0
  22. data/lib/mongo_mapper/document.rb +496 -0
  23. data/lib/mongo_mapper/dynamic_finder.rb +74 -0
  24. data/lib/mongo_mapper/embedded_document.rb +380 -0
  25. data/lib/mongo_mapper/finder_options.rb +145 -0
  26. data/lib/mongo_mapper/key.rb +36 -0
  27. data/lib/mongo_mapper/mongo_mapper.rb +125 -0
  28. data/lib/mongo_mapper/pagination.rb +66 -0
  29. data/lib/mongo_mapper/rails_compatibility/document.rb +15 -0
  30. data/lib/mongo_mapper/rails_compatibility/embedded_document.rb +28 -0
  31. data/lib/mongo_mapper/serialization.rb +54 -0
  32. data/lib/mongo_mapper/serializers/json_serializer.rb +48 -0
  33. data/lib/mongo_mapper/support.rb +192 -0
  34. data/lib/mongo_mapper/validations.rb +39 -0
  35. data/mongo_mapper.gemspec +173 -0
  36. data/specs.watchr +30 -0
  37. data/test/NOTE_ON_TESTING +1 -0
  38. data/test/functional/associations/test_belongs_to_polymorphic_proxy.rb +55 -0
  39. data/test/functional/associations/test_belongs_to_proxy.rb +91 -0
  40. data/test/functional/associations/test_many_documents_as_proxy.rb +246 -0
  41. data/test/functional/associations/test_many_documents_proxy.rb +477 -0
  42. data/test/functional/associations/test_many_embedded_polymorphic_proxy.rb +156 -0
  43. data/test/functional/associations/test_many_embedded_proxy.rb +192 -0
  44. data/test/functional/associations/test_many_polymorphic_proxy.rb +339 -0
  45. data/test/functional/associations/test_one_proxy.rb +131 -0
  46. data/test/functional/test_associations.rb +44 -0
  47. data/test/functional/test_binary.rb +33 -0
  48. data/test/functional/test_callbacks.rb +85 -0
  49. data/test/functional/test_dirty.rb +159 -0
  50. data/test/functional/test_document.rb +1198 -0
  51. data/test/functional/test_embedded_document.rb +135 -0
  52. data/test/functional/test_logger.rb +20 -0
  53. data/test/functional/test_modifiers.rb +242 -0
  54. data/test/functional/test_pagination.rb +95 -0
  55. data/test/functional/test_rails_compatibility.rb +25 -0
  56. data/test/functional/test_string_id_compatibility.rb +72 -0
  57. data/test/functional/test_validations.rb +361 -0
  58. data/test/models.rb +271 -0
  59. data/test/support/custom_matchers.rb +55 -0
  60. data/test/support/timing.rb +16 -0
  61. data/test/test_helper.rb +27 -0
  62. data/test/unit/associations/test_base.rb +182 -0
  63. data/test/unit/associations/test_proxy.rb +91 -0
  64. data/test/unit/serializers/test_json_serializer.rb +189 -0
  65. data/test/unit/test_document.rb +236 -0
  66. data/test/unit/test_dynamic_finder.rb +125 -0
  67. data/test/unit/test_embedded_document.rb +709 -0
  68. data/test/unit/test_finder_options.rb +325 -0
  69. data/test/unit/test_key.rb +172 -0
  70. data/test/unit/test_mongo_mapper.rb +65 -0
  71. data/test/unit/test_pagination.rb +119 -0
  72. data/test/unit/test_rails_compatibility.rb +52 -0
  73. data/test/unit/test_serializations.rb +52 -0
  74. data/test/unit/test_support.rb +346 -0
  75. data/test/unit/test_time_zones.rb +40 -0
  76. data/test/unit/test_validations.rb +503 -0
  77. metadata +239 -0
@@ -0,0 +1,61 @@
1
+ module MongoMapper
2
+ module Callbacks
3
+ def self.included(model)
4
+ model.class_eval do
5
+ include ActiveSupport::Callbacks
6
+
7
+ define_callbacks(
8
+ :before_save, :after_save,
9
+ :before_create, :after_create,
10
+ :before_update, :after_update,
11
+ :before_validation, :after_validation,
12
+ :before_validation_on_create, :after_validation_on_create,
13
+ :before_validation_on_update, :after_validation_on_update,
14
+ :before_destroy, :after_destroy
15
+ )
16
+ end
17
+ end
18
+
19
+ def valid?
20
+ action = new? ? 'create' : 'update'
21
+
22
+ run_callbacks(:before_validation)
23
+ run_callbacks("before_validation_on_#{action}".to_sym)
24
+ result = super
25
+ run_callbacks("after_validation_on_#{action}".to_sym)
26
+ run_callbacks(:after_validation)
27
+
28
+ result
29
+ end
30
+
31
+ def destroy
32
+ run_callbacks(:before_destroy)
33
+ result = super
34
+ run_callbacks(:after_destroy)
35
+ result
36
+ end
37
+
38
+ private
39
+ def create_or_update(*args)
40
+ run_callbacks(:before_save)
41
+ if result = super
42
+ run_callbacks(:after_save)
43
+ end
44
+ result
45
+ end
46
+
47
+ def create(*args)
48
+ run_callbacks(:before_create)
49
+ result = super
50
+ run_callbacks(:after_create)
51
+ result
52
+ end
53
+
54
+ def update(*args)
55
+ run_callbacks(:before_update)
56
+ result = super
57
+ run_callbacks(:after_update)
58
+ result
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,117 @@
1
+ module MongoMapper
2
+ module Dirty
3
+ DIRTY_SUFFIXES = ['_changed?', '_change', '_will_change!', '_was']
4
+
5
+ def method_missing(method, *args, &block)
6
+ if method.to_s =~ /(_changed\?|_change|_will_change!|_was)$/
7
+ method_suffix = $1
8
+ key = method.to_s.gsub(method_suffix, '')
9
+
10
+ if key_names.include?(key)
11
+ case method_suffix
12
+ when '_changed?'
13
+ key_changed?(key)
14
+ when '_change'
15
+ key_change(key)
16
+ when '_will_change!'
17
+ key_will_change!(key)
18
+ when '_was'
19
+ key_was(key)
20
+ end
21
+ else
22
+ super
23
+ end
24
+ else
25
+ super
26
+ end
27
+ end
28
+
29
+ def changed?
30
+ !changed_keys.empty?
31
+ end
32
+
33
+ def changed
34
+ changed_keys.keys
35
+ end
36
+
37
+ def changes
38
+ changed.inject({}) { |h, attribute| h[attribute] = key_change(attribute); h }
39
+ end
40
+
41
+ def initialize(attrs={})
42
+ super(attrs)
43
+ changed_keys.clear unless new?
44
+ end
45
+
46
+ def save(*args)
47
+ if status = super
48
+ changed_keys.clear
49
+ end
50
+ status
51
+ end
52
+
53
+ def save!(*args)
54
+ status = super
55
+ changed_keys.clear
56
+ status
57
+ end
58
+
59
+ def reload(*args)
60
+ record = super
61
+ changed_keys.clear
62
+ record
63
+ end
64
+
65
+ private
66
+ def clone_key_value(attribute_name)
67
+ value = send(:read_attribute, attribute_name)
68
+ value.duplicable? ? value.clone : value
69
+ rescue TypeError, NoMethodError
70
+ value
71
+ end
72
+
73
+ def changed_keys
74
+ @changed_keys ||= {}
75
+ end
76
+
77
+ def key_changed?(attribute)
78
+ changed_keys.include?(attribute)
79
+ end
80
+
81
+ def key_change(attribute)
82
+ [changed_keys[attribute], __send__(attribute)] if key_changed?(attribute)
83
+ end
84
+
85
+ def key_was(attribute)
86
+ key_changed?(attribute) ? changed_keys[attribute] : __send__(attribute)
87
+ end
88
+
89
+ def key_will_change!(attribute)
90
+ changed_keys[attribute] = clone_key_value(attribute)
91
+ end
92
+
93
+ def write_attribute(attribute, value)
94
+ attribute = attribute.to_s
95
+
96
+ if changed_keys.include?(attribute)
97
+ old = changed_keys[attribute]
98
+ changed_keys.delete(attribute) unless value_changed?(attribute, old, value)
99
+ else
100
+ old = clone_key_value(attribute)
101
+ changed_keys[attribute] = old if value_changed?(attribute, old, value)
102
+ end
103
+
104
+ super(attribute, value)
105
+ end
106
+
107
+ def value_changed?(key_name, old, value)
108
+ key = _keys[key_name]
109
+
110
+ if key.number? && value.blank?
111
+ value = nil
112
+ end
113
+
114
+ old != value
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,496 @@
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 Callbacks
10
+ include Dirty
11
+ include RailsCompatibility::Document
12
+ extend Validations::Macros
13
+ extend ClassMethods
14
+ extend Finders
15
+
16
+ def self.per_page
17
+ 25
18
+ end unless respond_to?(:per_page)
19
+ end
20
+
21
+ extra_extensions.each { |extension| model.extend(extension) }
22
+ extra_inclusions.each { |inclusion| model.send(:include, inclusion) }
23
+
24
+ descendants << model
25
+ end
26
+
27
+ def self.descendants
28
+ @descendants ||= Set.new
29
+ end
30
+
31
+ def self.append_extensions(*extensions)
32
+ extra_extensions.concat extensions
33
+
34
+ # Add the extension to existing descendants
35
+ descendants.each do |model|
36
+ extensions.each { |extension| model.extend(extension) }
37
+ end
38
+ end
39
+
40
+ # @api private
41
+ def self.extra_extensions
42
+ @extra_extensions ||= []
43
+ end
44
+
45
+ def self.append_inclusions(*inclusions)
46
+ extra_inclusions.concat inclusions
47
+
48
+ # Add the inclusion to existing descendants
49
+ descendants.each do |model|
50
+ inclusions.each { |inclusion| model.send :include, inclusion }
51
+ end
52
+ end
53
+
54
+ # @api private
55
+ def self.extra_inclusions
56
+ @extra_inclusions ||= []
57
+ end
58
+
59
+ module ClassMethods
60
+ def key(*args)
61
+ key = super
62
+ create_indexes_for(key)
63
+ key
64
+ end
65
+
66
+ def ensure_index(name_or_array, options={})
67
+ keys_to_index = if name_or_array.is_a?(Array)
68
+ name_or_array.map { |pair| [pair[0], pair[1]] }
69
+ else
70
+ name_or_array
71
+ end
72
+
73
+ MongoMapper.ensure_index(self, keys_to_index, options)
74
+ end
75
+
76
+ def find!(*args)
77
+ options = args.extract_options!
78
+ case args.first
79
+ when :first then first(options)
80
+ when :last then last(options)
81
+ when :all then find_every(options)
82
+ when Array then find_some(args, options)
83
+ else
84
+ case args.size
85
+ when 0
86
+ raise DocumentNotFound, "Couldn't find without an ID"
87
+ when 1
88
+ find_one!(options.merge({:_id => args[0]}))
89
+ else
90
+ find_some(args, options)
91
+ end
92
+ end
93
+ end
94
+
95
+ def find(*args)
96
+ find!(*args)
97
+ rescue DocumentNotFound
98
+ nil
99
+ end
100
+
101
+ def paginate(options)
102
+ per_page = options.delete(:per_page) || self.per_page
103
+ page = options.delete(:page)
104
+ total_entries = count(options)
105
+ pagination = Pagination::PaginationProxy.new(total_entries, page, per_page)
106
+
107
+ options.merge!(:limit => pagination.limit, :skip => pagination.skip)
108
+ pagination.subject = find_every(options)
109
+ pagination
110
+ end
111
+
112
+ def first(options={})
113
+ find_one(options)
114
+ end
115
+
116
+ def last(options={})
117
+ raise ':order option must be provided when using last' if options[:order].blank?
118
+ find_one(options.merge(:order => invert_order_clause(options[:order])))
119
+ end
120
+
121
+ def all(options={})
122
+ find_every(options)
123
+ end
124
+
125
+ def find_by_id(id)
126
+ find_one(:_id => id)
127
+ end
128
+
129
+ def count(options={})
130
+ collection.find(to_criteria(options)).count
131
+ end
132
+
133
+ def exists?(options={})
134
+ !count(options).zero?
135
+ end
136
+
137
+ def create(*docs)
138
+ initialize_each(*docs) { |doc| doc.save }
139
+ end
140
+
141
+ def create!(*docs)
142
+ initialize_each(*docs) { |doc| doc.save! }
143
+ end
144
+
145
+ def update(*args)
146
+ if args.length == 1
147
+ update_multiple(args[0])
148
+ else
149
+ id, attributes = args
150
+ update_single(id, attributes)
151
+ end
152
+ end
153
+
154
+ def delete(*ids)
155
+ collection.remove(to_criteria(:_id => ids.flatten))
156
+ end
157
+
158
+ def delete_all(options={})
159
+ collection.remove(to_criteria(options))
160
+ end
161
+
162
+ def destroy(*ids)
163
+ find_some(ids.flatten).each(&:destroy)
164
+ end
165
+
166
+ def destroy_all(options={})
167
+ all(options).each(&:destroy)
168
+ end
169
+
170
+ def increment(*args)
171
+ modifier_update('$inc', args)
172
+ end
173
+
174
+ def decrement(*args)
175
+ criteria, keys = criteria_and_keys_from_args(args)
176
+ values, to_decrement = keys.values, {}
177
+ keys.keys.each_with_index { |k, i| to_decrement[k] = -values[i].abs }
178
+ collection.update(criteria, {'$inc' => to_decrement}, :multi => true)
179
+ end
180
+
181
+ def set(*args)
182
+ modifier_update('$set', args)
183
+ end
184
+
185
+ def push(*args)
186
+ modifier_update('$push', args)
187
+ end
188
+
189
+ def push_all(*args)
190
+ modifier_update('$pushAll', args)
191
+ end
192
+
193
+ def push_uniq(*args)
194
+ criteria, keys = criteria_and_keys_from_args(args)
195
+ keys.each { |key, value | criteria[key] = {'$ne' => value} }
196
+ collection.update(criteria, {'$push' => keys}, :multi => true)
197
+ end
198
+
199
+ def pull(*args)
200
+ modifier_update('$pull', args)
201
+ end
202
+
203
+ def pull_all(*args)
204
+ modifier_update('$pullAll', args)
205
+ end
206
+
207
+ def modifier_update(modifier, args)
208
+ criteria, keys = criteria_and_keys_from_args(args)
209
+ modifiers = {modifier => keys}
210
+ collection.update(criteria, modifiers, :multi => true)
211
+ end
212
+ private :modifier_update
213
+
214
+ def criteria_and_keys_from_args(args)
215
+ keys = args.pop
216
+ criteria = args[0].is_a?(Hash) ? args[0] : {:id => args}
217
+ [to_criteria(criteria), keys]
218
+ end
219
+ private :criteria_and_keys_from_args
220
+
221
+ def connection(mongo_connection=nil)
222
+ if mongo_connection.nil?
223
+ @connection ||= MongoMapper.connection
224
+ else
225
+ @connection = mongo_connection
226
+ end
227
+ @connection
228
+ end
229
+
230
+ def set_database_name(name)
231
+ @database_name = name
232
+ end
233
+
234
+ def database_name
235
+ @database_name
236
+ end
237
+
238
+ def database
239
+ if database_name.nil?
240
+ MongoMapper.database
241
+ else
242
+ connection.db(database_name)
243
+ end
244
+ end
245
+
246
+ def set_collection_name(name)
247
+ @collection_name = name
248
+ end
249
+
250
+ def collection_name
251
+ @collection_name ||= self.to_s.tableize.gsub(/\//, '.')
252
+ end
253
+
254
+ def collection
255
+ database.collection(collection_name)
256
+ end
257
+
258
+ def timestamps!
259
+ key :created_at, Time
260
+ key :updated_at, Time
261
+ class_eval { before_save :update_timestamps }
262
+ end
263
+
264
+ def locking!(version_field=:_rev)
265
+ @lock_version_field = version_field
266
+ key version_field, String if version_field
267
+ end
268
+
269
+ def lock_version_field
270
+ @lock_version_field ||= false
271
+ end
272
+
273
+ def userstamps!
274
+ key :creator_id, ObjectId
275
+ key :updater_id, ObjectId
276
+ belongs_to :creator, :class_name => 'User'
277
+ belongs_to :updater, :class_name => 'User'
278
+ end
279
+
280
+ def single_collection_inherited?
281
+ keys.has_key?('_type') && single_collection_inherited_superclass?
282
+ end
283
+
284
+ def single_collection_inherited_superclass?
285
+ superclass.respond_to?(:keys) && superclass.keys.has_key?('_type')
286
+ end
287
+
288
+ private
289
+ def create_indexes_for(key)
290
+ ensure_index key.name if key.options[:index]
291
+ end
292
+
293
+ def initialize_each(*docs)
294
+ instances = []
295
+ docs = [{}] if docs.blank?
296
+ docs.flatten.each do |attrs|
297
+ doc = initialize_doc(attrs)
298
+ yield(doc)
299
+ instances << doc
300
+ end
301
+ instances.size == 1 ? instances[0] : instances
302
+ end
303
+
304
+ def find_every(options)
305
+ criteria, options = to_finder_options(options)
306
+ collection.find(criteria, options).to_a.map do |doc|
307
+ initialize_doc(doc)
308
+ end
309
+ end
310
+
311
+ def find_some(ids, options={})
312
+ ids = ids.flatten.compact.uniq
313
+ documents = find_every(options.merge(:_id => ids))
314
+
315
+ if ids.size == documents.size
316
+ documents
317
+ else
318
+ raise DocumentNotFound, "Couldn't find all of the ids (#{ids.to_sentence}). Found #{documents.size}, but was expecting #{ids.size}"
319
+ end
320
+ end
321
+
322
+ def find_one(options={})
323
+ criteria, options = to_finder_options(options)
324
+ if doc = collection.find_one(criteria, options)
325
+ initialize_doc(doc)
326
+ end
327
+ end
328
+
329
+ def find_one!(options={})
330
+ find_one(options) || raise(DocumentNotFound, "Document match #{options.inspect} does not exist in #{collection.name} collection")
331
+ end
332
+
333
+ def invert_order_clause(order)
334
+ order.split(',').map do |order_segment|
335
+ if order_segment =~ /\sasc/i
336
+ order_segment.sub /\sasc/i, ' desc'
337
+ elsif order_segment =~ /\sdesc/i
338
+ order_segment.sub /\sdesc/i, ' asc'
339
+ else
340
+ "#{order_segment.strip} desc"
341
+ end
342
+ end.join(',')
343
+ end
344
+
345
+ def update_single(id, attrs)
346
+ if id.blank? || attrs.blank? || !attrs.is_a?(Hash)
347
+ raise ArgumentError, "Updating a single document requires an id and a hash of attributes"
348
+ end
349
+
350
+ doc = find(id)
351
+ doc.update_attributes(attrs)
352
+ doc
353
+ end
354
+
355
+ def update_multiple(docs)
356
+ unless docs.is_a?(Hash)
357
+ raise ArgumentError, "Updating multiple documents takes 1 argument and it must be hash"
358
+ end
359
+
360
+ instances = []
361
+ docs.each_pair { |id, attrs| instances << update(id, attrs) }
362
+ instances
363
+ end
364
+
365
+ def to_criteria(options={})
366
+ FinderOptions.new(self, options).criteria
367
+ end
368
+
369
+ def to_finder_options(options={})
370
+ FinderOptions.new(self, options).to_a
371
+ end
372
+ end
373
+
374
+ module InstanceMethods
375
+ def collection
376
+ self.class.collection
377
+ end
378
+
379
+ def database
380
+ self.class.database
381
+ end
382
+
383
+ def lock_version_field
384
+ self.class.lock_version_field
385
+ end
386
+
387
+ def new?
388
+ read_attribute('_id').blank? || using_custom_id?
389
+ end
390
+
391
+ def save(options={})
392
+ if options === false
393
+ ActiveSupport::Deprecation.warn "save with true/false is deprecated. You should now use :validate => true/false."
394
+ options = {:validate => false}
395
+ end
396
+ options.reverse_merge!(:validate => true)
397
+ perform_validations = options.delete(:validate)
398
+ !perform_validations || valid? ? create_or_update(options) : false
399
+ end
400
+
401
+ def save!
402
+ save || raise(DocumentNotValid.new(self))
403
+ end
404
+
405
+ def destroy
406
+ self.class.delete(id) unless new?
407
+ end
408
+
409
+ def delete
410
+ self.class.delete(id) unless new?
411
+ end
412
+
413
+ def reload
414
+ doc = self.class.find(_id)
415
+ self.class.associations.each { |name, assoc| send(name).reset if respond_to?(name) }
416
+ self.attributes = doc.attributes
417
+ self
418
+ end
419
+
420
+ private
421
+ def create_or_update(options={})
422
+ result = new? ? create(options) : update(options)
423
+ result != false
424
+ end
425
+
426
+ def create(options={})
427
+ assign_id
428
+ save_to_collection(options)
429
+ end
430
+
431
+ def assign_id
432
+ if read_attribute(:_id).blank?
433
+ write_attribute :_id, Mongo::ObjectID.new
434
+ end
435
+ end
436
+
437
+ def prep_lock_version
438
+ if lock_version_field
439
+ old = read_attribute(lock_version_field)
440
+ v = Time.now.to_f
441
+ write_attribute lock_version_field, v
442
+ old
443
+ else
444
+ false
445
+ end
446
+ end
447
+
448
+ def update(options={})
449
+ save_to_collection(options)
450
+ end
451
+
452
+ def save_to_collection(options={})
453
+ clear_custom_id_flag
454
+ current_version = prep_lock_version
455
+
456
+ safe = options.delete(:safe)
457
+ safe = MongoMapper.safe_mode if safe.nil?
458
+
459
+ selector = { :_id => read_attribute(:_id) }
460
+ upsert = true
461
+
462
+ if current_version
463
+ selector[lock_version_field] = current_version
464
+ safe = true
465
+ upsert = false
466
+ end
467
+
468
+ if new?
469
+ collection.insert(to_mongo, :safe => safe)
470
+ else
471
+ result = collection.update(selector, to_mongo, :upsert => upsert, :safe => safe)
472
+
473
+ if result.is_a? Array
474
+ if current_version && result[0][0]['updatedExisting'] == false
475
+ raise StaleDocumentError.new
476
+ end
477
+ elsif lock_version_field
478
+ #raise some error
479
+ end
480
+
481
+ selector[:_id]
482
+ end
483
+ end
484
+
485
+ def update_timestamps
486
+ now = Time.now.utc
487
+ write_attribute('created_at', now) if new? && read_attribute('created_at').blank?
488
+ write_attribute('updated_at', now)
489
+ end
490
+
491
+ def clear_custom_id_flag
492
+ @using_custom_id = nil
493
+ end
494
+ end
495
+ end # Document
496
+ end # MongoMapper