tpitale-mongo_mapper 0.6.9

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