tpitale-mongo_mapper 0.6.9

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 (75) hide show
  1. data/.gitignore +10 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +53 -0
  4. data/Rakefile +55 -0
  5. data/VERSION +1 -0
  6. data/bin/mmconsole +60 -0
  7. data/lib/mongo_mapper/associations/base.rb +110 -0
  8. data/lib/mongo_mapper/associations/belongs_to_polymorphic_proxy.rb +26 -0
  9. data/lib/mongo_mapper/associations/belongs_to_proxy.rb +21 -0
  10. data/lib/mongo_mapper/associations/collection.rb +19 -0
  11. data/lib/mongo_mapper/associations/many_documents_as_proxy.rb +26 -0
  12. data/lib/mongo_mapper/associations/many_documents_proxy.rb +115 -0
  13. data/lib/mongo_mapper/associations/many_embedded_polymorphic_proxy.rb +31 -0
  14. data/lib/mongo_mapper/associations/many_embedded_proxy.rb +54 -0
  15. data/lib/mongo_mapper/associations/many_polymorphic_proxy.rb +11 -0
  16. data/lib/mongo_mapper/associations/proxy.rb +113 -0
  17. data/lib/mongo_mapper/associations.rb +70 -0
  18. data/lib/mongo_mapper/callbacks.rb +109 -0
  19. data/lib/mongo_mapper/dirty.rb +136 -0
  20. data/lib/mongo_mapper/document.rb +472 -0
  21. data/lib/mongo_mapper/dynamic_finder.rb +74 -0
  22. data/lib/mongo_mapper/embedded_document.rb +384 -0
  23. data/lib/mongo_mapper/finder_options.rb +133 -0
  24. data/lib/mongo_mapper/key.rb +36 -0
  25. data/lib/mongo_mapper/observing.rb +50 -0
  26. data/lib/mongo_mapper/pagination.rb +55 -0
  27. data/lib/mongo_mapper/rails_compatibility/document.rb +15 -0
  28. data/lib/mongo_mapper/rails_compatibility/embedded_document.rb +27 -0
  29. data/lib/mongo_mapper/serialization.rb +54 -0
  30. data/lib/mongo_mapper/serializers/json_serializer.rb +92 -0
  31. data/lib/mongo_mapper/support.rb +206 -0
  32. data/lib/mongo_mapper/validations.rb +41 -0
  33. data/lib/mongo_mapper.rb +120 -0
  34. data/mongo_mapper.gemspec +173 -0
  35. data/specs.watchr +32 -0
  36. data/test/NOTE_ON_TESTING +1 -0
  37. data/test/functional/associations/test_belongs_to_polymorphic_proxy.rb +55 -0
  38. data/test/functional/associations/test_belongs_to_proxy.rb +48 -0
  39. data/test/functional/associations/test_many_documents_as_proxy.rb +246 -0
  40. data/test/functional/associations/test_many_documents_proxy.rb +387 -0
  41. data/test/functional/associations/test_many_embedded_polymorphic_proxy.rb +156 -0
  42. data/test/functional/associations/test_many_embedded_proxy.rb +192 -0
  43. data/test/functional/associations/test_many_polymorphic_proxy.rb +339 -0
  44. data/test/functional/test_associations.rb +44 -0
  45. data/test/functional/test_binary.rb +18 -0
  46. data/test/functional/test_callbacks.rb +85 -0
  47. data/test/functional/test_dirty.rb +159 -0
  48. data/test/functional/test_document.rb +1235 -0
  49. data/test/functional/test_embedded_document.rb +135 -0
  50. data/test/functional/test_logger.rb +20 -0
  51. data/test/functional/test_pagination.rb +95 -0
  52. data/test/functional/test_rails_compatibility.rb +25 -0
  53. data/test/functional/test_string_id_compatibility.rb +72 -0
  54. data/test/functional/test_validations.rb +378 -0
  55. data/test/models.rb +271 -0
  56. data/test/support/custom_matchers.rb +55 -0
  57. data/test/support/timing.rb +16 -0
  58. data/test/test_helper.rb +27 -0
  59. data/test/unit/associations/test_base.rb +166 -0
  60. data/test/unit/associations/test_proxy.rb +91 -0
  61. data/test/unit/serializers/test_json_serializer.rb +189 -0
  62. data/test/unit/test_document.rb +204 -0
  63. data/test/unit/test_dynamic_finder.rb +125 -0
  64. data/test/unit/test_embedded_document.rb +718 -0
  65. data/test/unit/test_finder_options.rb +296 -0
  66. data/test/unit/test_key.rb +172 -0
  67. data/test/unit/test_mongo_mapper.rb +65 -0
  68. data/test/unit/test_observing.rb +101 -0
  69. data/test/unit/test_pagination.rb +113 -0
  70. data/test/unit/test_rails_compatibility.rb +49 -0
  71. data/test/unit/test_serializations.rb +52 -0
  72. data/test/unit/test_support.rb +342 -0
  73. data/test/unit/test_time_zones.rb +40 -0
  74. data/test/unit/test_validations.rb +503 -0
  75. metadata +235 -0
@@ -0,0 +1,472 @@
1
+ require 'set'
2
+
3
+ module MongoMapper
4
+ module Document
5
+ def self.included(model)
6
+ model.class_eval do
7
+ include EmbeddedDocument
8
+ include InstanceMethods
9
+ include Observing
10
+ include Callbacks
11
+ include Dirty
12
+ include RailsCompatibility::Document
13
+ extend Validations::Macros
14
+ extend ClassMethods
15
+ extend Finders
16
+
17
+ def self.per_page
18
+ 25
19
+ end unless respond_to?(:per_page)
20
+ end
21
+
22
+ descendants << model
23
+ end
24
+
25
+ def self.descendants
26
+ @descendants ||= Set.new
27
+ end
28
+
29
+ module ClassMethods
30
+ def key(*args)
31
+ key = super
32
+ create_indexes_for(key)
33
+ key
34
+ end
35
+
36
+ def ensure_index(name_or_array, options={})
37
+ keys_to_index = if name_or_array.is_a?(Array)
38
+ name_or_array.map { |pair| [pair[0], pair[1]] }
39
+ else
40
+ name_or_array
41
+ end
42
+
43
+ MongoMapper.ensure_index(self, keys_to_index, options)
44
+ end
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
58
+ def find!(*args)
59
+ options = args.extract_options!
60
+ case args.first
61
+ when :first then first(options)
62
+ when :last then last(options)
63
+ when :all then find_every(options)
64
+ when Array then find_some(args, options)
65
+ else
66
+ case args.size
67
+ when 0
68
+ raise DocumentNotFound, "Couldn't find without an ID"
69
+ when 1
70
+ find_one!(options.merge({:_id => args[0]}))
71
+ else
72
+ find_some(args, options)
73
+ end
74
+ end
75
+ end
76
+
77
+ def find(*args)
78
+ find!(*args)
79
+ rescue DocumentNotFound
80
+ nil
81
+ end
82
+
83
+ def paginate(options)
84
+ per_page = options.delete(:per_page) || self.per_page
85
+ page = options.delete(:page)
86
+ total_entries = count(options)
87
+ pagination = Pagination::PaginationProxy.new(total_entries, page, per_page)
88
+
89
+ options.merge!(:limit => pagination.limit, :skip => pagination.skip)
90
+ pagination.subject = find_every(options)
91
+ pagination
92
+ end
93
+
94
+ # @param [Hash] options any conditions understood by
95
+ # FinderOptions.to_mongo_criteria
96
+ #
97
+ # @return the first document in the ordered collection as described by
98
+ # +options+
99
+ #
100
+ # @see FinderOptions
101
+ def first(options={})
102
+ find_one(options)
103
+ end
104
+
105
+ # @param [Hash] options any conditions understood by
106
+ # FinderOptions.to_mongo_criteria
107
+ # @option [String] :order this *mandatory* option describes how to
108
+ # identify the ordering of the documents in your collection. Note that
109
+ # the *last* document in this collection will be selected.
110
+ #
111
+ # @return the last document in the ordered collection as described by
112
+ # +options+
113
+ #
114
+ # @raise Exception when no <tt>:order</tt> option has been defined
115
+ def last(options={})
116
+ raise ':order option must be provided when using last' if options[:order].blank?
117
+ find_one(options.merge(:order => invert_order_clause(options[:order])))
118
+ end
119
+
120
+ # @param [Hash] options any conditions understood by
121
+ # FinderOptions.to_mongo_criteria
122
+ #
123
+ # @return [Array] all documents in your collection that match the
124
+ # provided conditions
125
+ #
126
+ # @see FinderOptions
127
+ def all(options={})
128
+ find_every(options)
129
+ end
130
+
131
+ def find_by_id(id)
132
+ find_one(:_id => id)
133
+ end
134
+
135
+ def count(options={})
136
+ collection.find(to_criteria(options)).count
137
+ end
138
+
139
+ def exists?(options={})
140
+ !count(options).zero?
141
+ end
142
+
143
+ # @overload create(doc_attributes)
144
+ # Create a single new document
145
+ # @param [Hash] doc_attributes key/value pairs to create a new
146
+ # document
147
+ #
148
+ # @overload create(docs_attributes)
149
+ # Create many new documents
150
+ # @param [Array<Hash>] provide many Hashes of key/value pairs to create
151
+ # multiple documents
152
+ #
153
+ # @example Creating a single document
154
+ # MyModel.create({ :foo => "bar" })
155
+ #
156
+ # @example Creating multiple documents
157
+ # MyModel.create([{ :foo => "bar" }, { :foo => "baz" })
158
+ #
159
+ # @return [Boolean] when a document is successfully created, +true+ will
160
+ # be returned. If a document fails to create, +false+ will be returned.
161
+ def create(*docs)
162
+ initialize_each(*docs) { |doc| doc.save }
163
+ end
164
+
165
+ # @see Document.create
166
+ #
167
+ # @raise [DocumentNotValid] raised if a document fails to create
168
+ def create!(*docs)
169
+ initialize_each(*docs) { |doc| doc.save! }
170
+ end
171
+
172
+ # @overload update(id, attributes)
173
+ # Update a single document
174
+ # @param id the ID of the document you wish to update
175
+ # @param [Hash] attributes the key to update on the document with a new
176
+ # value
177
+ #
178
+ # @overload update(ids_and_attributes)
179
+ # Update multiple documents
180
+ # @param [Hash] ids_and_attributes each key is the ID of some document
181
+ # you wish to update. The value each key points toward are those
182
+ # applied to the target document
183
+ #
184
+ # @example Updating single document
185
+ # Person.update(1, {:foo => 'bar'})
186
+ #
187
+ # @example Updating multiple documents at once:
188
+ # Person.update({'1' => {:foo => 'bar'}, '2' => {:baz => 'wick'}})
189
+ def update(*args)
190
+ if args.length == 1
191
+ update_multiple(args[0])
192
+ else
193
+ id, attributes = args
194
+ update_single(id, attributes)
195
+ end
196
+ end
197
+
198
+ # Removes ("deletes") one or many documents from the collection. Note
199
+ # that this will bypass any +destroy+ hooks defined by your class.
200
+ #
201
+ # @param [Array] ids the ID or IDs of the records you wish to delete
202
+ def delete(*ids)
203
+ collection.remove(to_criteria(:_id => ids.flatten))
204
+ end
205
+
206
+ def delete_all(options={})
207
+ collection.remove(to_criteria(options))
208
+ end
209
+
210
+ # Iterates over each document found by the provided IDs and calls their
211
+ # +destroy+ method. This has the advantage of processing your document's
212
+ # +destroy+ call-backs.
213
+ #
214
+ # @overload destroy(id)
215
+ # Destroy a single document by ID
216
+ # @param id the ID of the document to destroy
217
+ #
218
+ # @overload destroy(ids)
219
+ # Destroy many documents by their IDs
220
+ # @param [Array] the IDs of each document you wish to destroy
221
+ #
222
+ # @example Destroying a single document
223
+ # Person.destroy("34")
224
+ #
225
+ # @example Destroying multiple documents
226
+ # Person.destroy("34", "45", ..., "54")
227
+ #
228
+ # # OR...
229
+ #
230
+ # Person.destroy(["34", "45", ..., "54"])
231
+ def destroy(*ids)
232
+ find_some(ids.flatten).each(&:destroy)
233
+ end
234
+
235
+ def destroy_all(options={})
236
+ all(options).each(&:destroy)
237
+ end
238
+
239
+ # @overload connection()
240
+ # @return [Mongo::Connection] the connection used by your document class
241
+ #
242
+ # @overload connection(mongo_connection)
243
+ # @param [Mongo::Connection] mongo_connection a new connection for your
244
+ # document class to use
245
+ # @return [Mongo::Connection] a new Mongo::Connection for yoru document
246
+ # class
247
+ def connection(mongo_connection=nil)
248
+ if mongo_connection.nil?
249
+ @connection ||= MongoMapper.connection
250
+ else
251
+ @connection = mongo_connection
252
+ end
253
+ @connection
254
+ end
255
+
256
+ # Changes the database name from the default to whatever you want
257
+ #
258
+ # @param [#to_s] name the new database name to use.
259
+ def set_database_name(name)
260
+ @database_name = name
261
+ end
262
+
263
+ # Returns the database name
264
+ #
265
+ # @return [String] the database name
266
+ def database_name
267
+ @database_name
268
+ end
269
+
270
+ # Returns the database the document should use. Defaults to
271
+ # MongoMapper.database if other database is not set.
272
+ #
273
+ # @return [Mongo::DB] the mongo database instance
274
+ def database
275
+ if database_name.nil?
276
+ MongoMapper.database
277
+ else
278
+ connection.db(database_name)
279
+ end
280
+ end
281
+
282
+ # Changes the collection name from the default to whatever you want
283
+ #
284
+ # @param [#to_s] name the new collection name to use.
285
+ def set_collection_name(name)
286
+ @collection_name = name
287
+ end
288
+
289
+ # Returns the collection name, if not set, defaults to class name tableized
290
+ #
291
+ # @return [String] the collection name, if not set, defaults to class
292
+ # name tableized
293
+ def collection_name
294
+ @collection_name ||= self.to_s.tableize.gsub(/\//, '.')
295
+ end
296
+
297
+ # @return the Mongo Ruby driver +collection+ object
298
+ def collection
299
+ database.collection(collection_name)
300
+ end
301
+
302
+ # Defines a +created_at+ and +updated_at+ attribute (with a +Time+
303
+ # value) on your document. These attributes are updated by an
304
+ # injected +update_timestamps+ +before_save+ hook.
305
+ def timestamps!
306
+ key :created_at, Time
307
+ key :updated_at, Time
308
+ class_eval { before_save :update_timestamps }
309
+ end
310
+
311
+ def single_collection_inherited?
312
+ keys.has_key?('_type') && single_collection_inherited_superclass?
313
+ end
314
+
315
+ def single_collection_inherited_superclass?
316
+ superclass.respond_to?(:keys) && superclass.keys.has_key?('_type')
317
+ end
318
+
319
+ private
320
+ def create_indexes_for(key)
321
+ ensure_index key.name if key.options[:index]
322
+ end
323
+
324
+ def initialize_each(*docs)
325
+ instances = []
326
+ docs = [{}] if docs.blank?
327
+ docs.flatten.each do |attrs|
328
+ doc = initialize_doc(attrs)
329
+ yield(doc)
330
+ instances << doc
331
+ end
332
+ instances.size == 1 ? instances[0] : instances
333
+ end
334
+
335
+ def find_every(options)
336
+ criteria, options = to_finder_options(options)
337
+ collection.find(criteria, options).to_a.map do |doc|
338
+ initialize_doc(doc)
339
+ end
340
+ end
341
+
342
+ def find_some(ids, options={})
343
+ ids = ids.flatten.compact.uniq
344
+ documents = find_every(options.merge(:_id => ids))
345
+
346
+ if ids.size == documents.size
347
+ documents
348
+ else
349
+ raise DocumentNotFound, "Couldn't find all of the ids (#{ids.to_sentence}). Found #{documents.size}, but was expecting #{ids.size}"
350
+ end
351
+ end
352
+
353
+ def find_one(options={})
354
+ criteria, options = to_finder_options(options)
355
+ if doc = collection.find_one(criteria, options)
356
+ initialize_doc(doc)
357
+ end
358
+ end
359
+
360
+ def find_one!(options={})
361
+ find_one(options) || raise(DocumentNotFound, "Document match #{options.inspect} does not exist in #{collection.name} collection")
362
+ end
363
+
364
+ def invert_order_clause(order)
365
+ order.split(',').map do |order_segment|
366
+ if order_segment =~ /\sasc/i
367
+ order_segment.sub /\sasc/i, ' desc'
368
+ elsif order_segment =~ /\sdesc/i
369
+ order_segment.sub /\sdesc/i, ' asc'
370
+ else
371
+ "#{order_segment.strip} desc"
372
+ end
373
+ end.join(',')
374
+ end
375
+
376
+ def update_single(id, attrs)
377
+ if id.blank? || attrs.blank? || !attrs.is_a?(Hash)
378
+ raise ArgumentError, "Updating a single document requires an id and a hash of attributes"
379
+ end
380
+
381
+ doc = find(id)
382
+ doc.update_attributes(attrs)
383
+ doc
384
+ end
385
+
386
+ def update_multiple(docs)
387
+ unless docs.is_a?(Hash)
388
+ raise ArgumentError, "Updating multiple documents takes 1 argument and it must be hash"
389
+ end
390
+
391
+ instances = []
392
+ docs.each_pair { |id, attrs| instances << update(id, attrs) }
393
+ instances
394
+ end
395
+
396
+ def to_criteria(options={})
397
+ FinderOptions.new(self, options).criteria
398
+ end
399
+
400
+ def to_finder_options(options={})
401
+ FinderOptions.new(self, options).to_a
402
+ end
403
+ end
404
+
405
+ module InstanceMethods
406
+ def collection
407
+ self.class.collection
408
+ end
409
+
410
+ def new?
411
+ read_attribute('_id').blank? || using_custom_id?
412
+ end
413
+
414
+ def save(perform_validations=true)
415
+ !perform_validations || valid? ? create_or_update : false
416
+ end
417
+
418
+ def save!
419
+ save || raise(DocumentNotValid.new(self))
420
+ end
421
+
422
+ def destroy
423
+ return false if frozen?
424
+ self.class.delete(_id) unless new?
425
+ freeze
426
+ end
427
+
428
+ def reload
429
+ doc = self.class.find(_id)
430
+ self.class.associations.each { |name, assoc| send(name).reset if respond_to?(name) }
431
+ self.attributes = doc.attributes
432
+ self
433
+ end
434
+
435
+ private
436
+ def create_or_update
437
+ result = new? ? create : update
438
+ result != false
439
+ end
440
+
441
+ def create
442
+ assign_id
443
+ save_to_collection
444
+ end
445
+
446
+ def assign_id
447
+ if read_attribute(:_id).blank?
448
+ write_attribute :_id, Mongo::ObjectID.new
449
+ end
450
+ end
451
+
452
+ def update
453
+ save_to_collection
454
+ end
455
+
456
+ def save_to_collection
457
+ clear_custom_id_flag
458
+ collection.save(to_mongo)
459
+ end
460
+
461
+ def update_timestamps
462
+ now = Time.now.utc
463
+ write_attribute('created_at', now) if new? && read_attribute('created_at').blank?
464
+ write_attribute('updated_at', now)
465
+ end
466
+
467
+ def clear_custom_id_flag
468
+ @using_custom_id = nil
469
+ end
470
+ end
471
+ end # Document
472
+ end # MongoMapper
@@ -0,0 +1,74 @@
1
+ module MongoMapper
2
+ # @api private
3
+ module Finders
4
+ def dynamic_find(finder, args)
5
+ attributes = {}
6
+ finder.attributes.each_with_index do |attr, index|
7
+ attributes[attr] = args[index]
8
+ end
9
+
10
+ options = args.extract_options!.merge(attributes)
11
+
12
+ if result = find(finder.finder, options)
13
+ result
14
+ else
15
+ if finder.raise?
16
+ raise DocumentNotFound, "Couldn't find Document with #{attributes.inspect} in collection named #{collection.name}"
17
+ end
18
+
19
+ if finder.instantiator
20
+ self.send(finder.instantiator, attributes)
21
+ end
22
+ end
23
+ end
24
+
25
+ protected
26
+ def method_missing(method, *args, &block)
27
+ finder = DynamicFinder.new(method)
28
+
29
+ if finder.found?
30
+ dynamic_find(finder, args)
31
+ else
32
+ super
33
+ end
34
+ end
35
+ end
36
+
37
+ class DynamicFinder
38
+ attr_reader :method, :attributes, :finder, :bang, :instantiator
39
+
40
+ def initialize(method)
41
+ @method = method
42
+ @finder = :first
43
+ @bang = false
44
+ match
45
+ end
46
+
47
+ def found?
48
+ @finder.present?
49
+ end
50
+
51
+ def raise?
52
+ bang == true
53
+ end
54
+
55
+ protected
56
+ def match
57
+ case method.to_s
58
+ when /^find_(all_by|by)_([_a-zA-Z]\w*)$/
59
+ @finder = :all if $1 == 'all_by'
60
+ names = $2
61
+ when /^find_by_([_a-zA-Z]\w*)\!$/
62
+ @bang = true
63
+ names = $1
64
+ when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
65
+ @instantiator = $1 == 'initialize' ? :new : :create
66
+ names = $2
67
+ else
68
+ @finder = nil
69
+ end
70
+
71
+ @attributes = names && names.split('_and_')
72
+ end
73
+ end
74
+ end