danielharan-mongo_mapper 0.6.5

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 (74) 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.rb +134 -0
  8. data/lib/mongo_mapper/associations.rb +183 -0
  9. data/lib/mongo_mapper/associations/base.rb +110 -0
  10. data/lib/mongo_mapper/associations/belongs_to_polymorphic_proxy.rb +34 -0
  11. data/lib/mongo_mapper/associations/belongs_to_proxy.rb +22 -0
  12. data/lib/mongo_mapper/associations/many_documents_as_proxy.rb +25 -0
  13. data/lib/mongo_mapper/associations/many_documents_proxy.rb +127 -0
  14. data/lib/mongo_mapper/associations/many_embedded_polymorphic_proxy.rb +33 -0
  15. data/lib/mongo_mapper/associations/many_embedded_proxy.rb +53 -0
  16. data/lib/mongo_mapper/associations/many_polymorphic_proxy.rb +11 -0
  17. data/lib/mongo_mapper/associations/many_proxy.rb +6 -0
  18. data/lib/mongo_mapper/associations/proxy.rb +80 -0
  19. data/lib/mongo_mapper/callbacks.rb +109 -0
  20. data/lib/mongo_mapper/dirty.rb +136 -0
  21. data/lib/mongo_mapper/document.rb +481 -0
  22. data/lib/mongo_mapper/dynamic_finder.rb +35 -0
  23. data/lib/mongo_mapper/embedded_document.rb +386 -0
  24. data/lib/mongo_mapper/finder_options.rb +133 -0
  25. data/lib/mongo_mapper/key.rb +36 -0
  26. data/lib/mongo_mapper/observing.rb +50 -0
  27. data/lib/mongo_mapper/pagination.rb +53 -0
  28. data/lib/mongo_mapper/rails_compatibility/document.rb +15 -0
  29. data/lib/mongo_mapper/rails_compatibility/embedded_document.rb +27 -0
  30. data/lib/mongo_mapper/serialization.rb +54 -0
  31. data/lib/mongo_mapper/serializers/json_serializer.rb +92 -0
  32. data/lib/mongo_mapper/support.rb +193 -0
  33. data/lib/mongo_mapper/validations.rb +41 -0
  34. data/mongo_mapper.gemspec +171 -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_embedded_polymorphic_proxy.rb +156 -0
  41. data/test/functional/associations/test_many_embedded_proxy.rb +196 -0
  42. data/test/functional/associations/test_many_polymorphic_proxy.rb +339 -0
  43. data/test/functional/associations/test_many_proxy.rb +384 -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 +1180 -0
  49. data/test/functional/test_embedded_document.rb +125 -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 +369 -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/serializers/test_json_serializer.rb +189 -0
  60. data/test/unit/test_association_base.rb +166 -0
  61. data/test/unit/test_document.rb +204 -0
  62. data/test/unit/test_dynamic_finder.rb +125 -0
  63. data/test/unit/test_embedded_document.rb +718 -0
  64. data/test/unit/test_finder_options.rb +296 -0
  65. data/test/unit/test_key.rb +172 -0
  66. data/test/unit/test_mongo_mapper.rb +65 -0
  67. data/test/unit/test_observing.rb +101 -0
  68. data/test/unit/test_pagination.rb +113 -0
  69. data/test/unit/test_rails_compatibility.rb +49 -0
  70. data/test/unit/test_serializations.rb +52 -0
  71. data/test/unit/test_support.rb +342 -0
  72. data/test/unit/test_time_zones.rb +40 -0
  73. data/test/unit/test_validations.rb +503 -0
  74. metadata +233 -0
@@ -0,0 +1,35 @@
1
+ module MongoMapper
2
+ class DynamicFinder
3
+ attr_reader :method, :attributes, :finder, :bang, :instantiator
4
+
5
+ def initialize(method)
6
+ @method = method
7
+ @finder = :first
8
+ @bang = false
9
+ match()
10
+ end
11
+
12
+ def found?
13
+ @finder.present?
14
+ end
15
+
16
+ protected
17
+ def match
18
+ case method.to_s
19
+ when /^find_(all_by|by)_([_a-zA-Z]\w*)$/
20
+ @finder = :all if $1 == 'all_by'
21
+ names = $2
22
+ when /^find_by_([_a-zA-Z]\w*)\!$/
23
+ @bang = true
24
+ names = $1
25
+ when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
26
+ @instantiator = $1 == 'initialize' ? :new : :create
27
+ names = $2
28
+ else
29
+ @finder = nil
30
+ end
31
+
32
+ @attributes = names && names.split('_and_')
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,386 @@
1
+ require 'observer'
2
+
3
+ module MongoMapper
4
+ module EmbeddedDocument
5
+ def self.included(model)
6
+ model.class_eval do
7
+ extend ClassMethods
8
+ include InstanceMethods
9
+
10
+ extend Associations::ClassMethods
11
+ include Associations::InstanceMethods
12
+
13
+ include RailsCompatibility::EmbeddedDocument
14
+ include Validatable
15
+ include Serialization
16
+
17
+ extend Validations::Macros
18
+
19
+ key :_id, ObjectId
20
+ attr_accessor :_root_document
21
+ end
22
+ end
23
+
24
+ module ClassMethods
25
+ def logger
26
+ MongoMapper.logger
27
+ end
28
+
29
+ def inherited(subclass)
30
+ unless subclass.embeddable?
31
+ subclass.set_collection_name(collection_name)
32
+ end
33
+
34
+ (@subclasses ||= []) << subclass
35
+ end
36
+
37
+ def subclasses
38
+ @subclasses
39
+ end
40
+
41
+ def keys
42
+ @keys ||= if parent = parent_model
43
+ parent.keys.dup
44
+ else
45
+ HashWithIndifferentAccess.new
46
+ end
47
+ end
48
+
49
+ def key(*args)
50
+ key = Key.new(*args)
51
+ keys[key.name] = key
52
+
53
+ create_accessors_for(key)
54
+ create_key_in_subclasses(*args)
55
+ create_validations_for(key)
56
+
57
+ key
58
+ end
59
+
60
+ def using_object_id?
61
+ object_id_key?(:_id)
62
+ end
63
+
64
+ def object_id_key?(name)
65
+ key = keys[name.to_s]
66
+ key && key.type == ObjectId
67
+ end
68
+
69
+ def embeddable?
70
+ !self.ancestors.include?(Document)
71
+ end
72
+
73
+ def parent_model
74
+ (ancestors - [self,EmbeddedDocument]).find do |parent_class|
75
+ parent_class.ancestors.include?(EmbeddedDocument)
76
+ end
77
+ end
78
+
79
+ def to_mongo(instance)
80
+ return nil if instance.nil?
81
+ instance.to_mongo
82
+ end
83
+
84
+ def from_mongo(instance_or_hash)
85
+ return nil if instance_or_hash.nil?
86
+
87
+ if instance_or_hash.is_a?(self)
88
+ instance_or_hash
89
+ else
90
+ initialize_doc(instance_or_hash)
91
+ end
92
+ end
93
+
94
+ private
95
+ def initialize_doc(doc)
96
+ begin
97
+ klass = doc['_type'].present? ? doc['_type'].constantize : self
98
+ klass.new(doc)
99
+ rescue NameError
100
+ new(doc)
101
+ end
102
+ end
103
+
104
+ def accessors_module
105
+ module_defined = if method(:const_defined?).arity == 1 # Ruby 1.9 compat check
106
+ const_defined?('MongoMapperKeys')
107
+ else
108
+ const_defined?('MongoMapperKeys', false)
109
+ end
110
+
111
+ if module_defined
112
+ const_get 'MongoMapperKeys'
113
+ else
114
+ const_set 'MongoMapperKeys', Module.new
115
+ end
116
+ end
117
+
118
+ def create_accessors_for(key)
119
+ accessors_module.module_eval <<-end_eval
120
+ def #{key.name}
121
+ read_attribute(:'#{key.name}')
122
+ end
123
+
124
+ def #{key.name}_before_typecast
125
+ read_attribute_before_typecast(:'#{key.name}')
126
+ end
127
+
128
+ def #{key.name}=(value)
129
+ write_attribute(:'#{key.name}', value)
130
+ end
131
+
132
+ def #{key.name}?
133
+ read_attribute(:#{key.name}).present?
134
+ end
135
+ end_eval
136
+ include accessors_module
137
+ end
138
+
139
+ def create_key_in_subclasses(*args)
140
+ return if subclasses.blank?
141
+
142
+ subclasses.each do |subclass|
143
+ subclass.key(*args)
144
+ end
145
+ end
146
+
147
+ def create_validations_for(key)
148
+ attribute = key.name.to_sym
149
+
150
+ if key.options[:required]
151
+ validates_presence_of(attribute)
152
+ end
153
+
154
+ if key.options[:unique]
155
+ validates_uniqueness_of(attribute)
156
+ end
157
+
158
+ if key.options[:numeric]
159
+ number_options = key.type == Integer ? {:only_integer => true} : {}
160
+ validates_numericality_of(attribute, number_options)
161
+ end
162
+
163
+ if key.options[:format]
164
+ validates_format_of(attribute, :with => key.options[:format])
165
+ end
166
+
167
+ if key.options[:length]
168
+ length_options = case key.options[:length]
169
+ when Integer
170
+ {:minimum => 0, :maximum => key.options[:length]}
171
+ when Range
172
+ {:within => key.options[:length]}
173
+ when Hash
174
+ key.options[:length]
175
+ end
176
+ validates_length_of(attribute, length_options)
177
+ end
178
+ end
179
+ end
180
+
181
+ module InstanceMethods
182
+ def logger
183
+ self.class.logger
184
+ end
185
+
186
+ def initialize(attrs={})
187
+ unless attrs.nil?
188
+ self.class.associations.each_pair do |name, association|
189
+ if collection = attrs.delete(name)
190
+ if association.many? && association.klass.embeddable?
191
+ root_document = attrs[:_root_document] || self
192
+ collection.each do |doc|
193
+ doc[:_root_document] = root_document
194
+ end
195
+ end
196
+ send("#{association.name}=", collection)
197
+ end
198
+ end
199
+
200
+ self.attributes = attrs
201
+
202
+ if respond_to?(:_type=) && self['_type'].blank?
203
+ self._type = self.class.name
204
+ end
205
+ end
206
+
207
+ if self.class.embeddable?
208
+ if read_attribute(:_id).blank?
209
+ write_attribute :_id, Mongo::ObjectID.new
210
+ @new_document = true
211
+ else
212
+ @new_document = false
213
+ end
214
+ end
215
+ end
216
+
217
+ def new?
218
+ !!@new_document
219
+ end
220
+
221
+ def to_param
222
+ id.to_s
223
+ end
224
+
225
+ def attributes=(attrs)
226
+ return if attrs.blank?
227
+ attrs.each_pair do |name, value|
228
+ writer_method = "#{name}="
229
+
230
+ if respond_to?(writer_method)
231
+ self.send(writer_method, value)
232
+ else
233
+ self[name.to_s] = value
234
+ end
235
+ end
236
+ end
237
+
238
+ def attributes
239
+ attrs = HashWithIndifferentAccess.new
240
+
241
+ embedded_keys.each do |key|
242
+ attrs[key.name] = read_attribute(key.name).try(:attributes)
243
+ end
244
+
245
+ non_embedded_keys.each do |key|
246
+ attrs[key.name] = read_attribute(key.name)
247
+ end
248
+
249
+ embedded_associations.each do |association|
250
+ documents = instance_variable_get(association.ivar)
251
+ next if documents.nil?
252
+ attrs[association.name] = documents.collect { |doc| doc.attributes }
253
+ end
254
+
255
+ attrs
256
+ end
257
+
258
+ def to_mongo
259
+ attrs = HashWithIndifferentAccess.new
260
+
261
+ _keys.each_pair do |name, key|
262
+ value = key.set(read_attribute(key.name))
263
+ attrs[name] = value unless value.nil?
264
+ end
265
+
266
+ embedded_associations.each do |association|
267
+ if documents = instance_variable_get(association.ivar)
268
+ attrs[association.name] = documents.map { |document| document.to_mongo }
269
+ end
270
+ end
271
+
272
+ attrs
273
+ end
274
+
275
+ def clone
276
+ clone_attributes = self.attributes
277
+ clone_attributes.delete("_id")
278
+ self.class.new(clone_attributes)
279
+ end
280
+
281
+ def [](name)
282
+ read_attribute(name)
283
+ end
284
+
285
+ def []=(name, value)
286
+ ensure_key_exists(name)
287
+ write_attribute(name, value)
288
+ end
289
+
290
+ def ==(other)
291
+ other.is_a?(self.class) && _id == other._id
292
+ end
293
+
294
+ def id
295
+ read_attribute(:_id)
296
+ end
297
+
298
+ def id=(value)
299
+ if self.class.using_object_id?
300
+ value = MongoMapper.normalize_object_id(value)
301
+ else
302
+ @using_custom_id = true
303
+ end
304
+
305
+ write_attribute :_id, value
306
+ end
307
+
308
+ def using_custom_id?
309
+ !!@using_custom_id
310
+ end
311
+
312
+ def inspect
313
+ attributes_as_nice_string = key_names.collect do |name|
314
+ "#{name}: #{read_attribute(name).inspect}"
315
+ end.join(", ")
316
+ "#<#{self.class} #{attributes_as_nice_string}>"
317
+ end
318
+
319
+ def save
320
+ if _root_document
321
+ _root_document.save
322
+ end
323
+ end
324
+
325
+ def save!
326
+ if _root_document
327
+ _root_document.save!
328
+ end
329
+ end
330
+
331
+ def update_attributes(attrs={})
332
+ self.attributes = attrs
333
+ save
334
+ end
335
+
336
+ private
337
+ def _keys
338
+ self.metaclass.keys
339
+ end
340
+
341
+ def key_names
342
+ _keys.keys
343
+ end
344
+
345
+ def non_embedded_keys
346
+ _keys.values.select { |key| !key.embeddable? }
347
+ end
348
+
349
+ def embedded_keys
350
+ _keys.values.select { |key| key.embeddable? }
351
+ end
352
+
353
+ def ensure_key_exists(name)
354
+ self.metaclass.key(name) unless respond_to?("#{name}=")
355
+ end
356
+
357
+ def read_attribute(name)
358
+ if key = _keys[name]
359
+ value = key.get(instance_variable_get("@#{name}"))
360
+ instance_variable_set "@#{name}", value if !frozen?
361
+ value
362
+ else
363
+ raise KeyNotFound, "Could not find key: #{name.inspect}"
364
+ end
365
+ end
366
+
367
+ def read_attribute_before_typecast(name)
368
+ instance_variable_get("@#{name}_before_typecast")
369
+ end
370
+
371
+ def write_attribute(name, value)
372
+ key = _keys[name]
373
+ instance_variable_set "@#{name}_before_typecast", value
374
+ instance_variable_set "@#{name}", key.set(value)
375
+ end
376
+
377
+ def embedded_associations
378
+ self.class.associations.select do |name, association|
379
+ association.embeddable?
380
+ end.map do |name, association|
381
+ association
382
+ end
383
+ end
384
+ end # InstanceMethods
385
+ end # EmbeddedDocument
386
+ end # MongoMapper
@@ -0,0 +1,133 @@
1
+ module MongoMapper
2
+ # Controls the parsing and handling of options used by finders.
3
+ #
4
+ # == Important Note
5
+ #
6
+ # This class is private to MongoMapper and should not be considered part of
7
+ # MongoMapper's public API. Some documentation herein, however, may prove
8
+ # useful for understanding how MongoMapper handles the parsing of finder
9
+ # conditions and options.
10
+ #
11
+ # @private
12
+ class FinderOptions
13
+ OptionKeys = [:fields, :select, :skip, :offset, :limit, :sort, :order]
14
+
15
+ def initialize(model, options)
16
+ raise ArgumentError, "Options must be a hash" unless options.is_a?(Hash)
17
+ options = options.symbolize_keys
18
+
19
+ @model = model
20
+ @options = {}
21
+ @conditions = options.delete(:conditions) || {}
22
+
23
+ options.each_pair do |key, value|
24
+ if OptionKeys.include?(key)
25
+ @options[key] = value
26
+ else
27
+ @conditions[key] = value
28
+ end
29
+ end
30
+
31
+ add_sci_scope
32
+ end
33
+
34
+ # @return [Hash] Mongo compatible criteria options
35
+ #
36
+ # @see FinderOptions#to_mongo_criteria
37
+ def criteria
38
+ to_mongo_criteria(@conditions)
39
+ end
40
+
41
+ # @return [Hash] Mongo compatible options
42
+ def options
43
+ fields = @options.delete(:fields) || @options.delete(:select)
44
+ skip = @options.delete(:skip) || @options.delete(:offset) || 0
45
+ limit = @options.delete(:limit) || 0
46
+ sort = @options.delete(:sort) || convert_order_to_sort(@options.delete(:order))
47
+
48
+ {:fields => to_mongo_fields(fields), :skip => skip.to_i, :limit => limit.to_i, :sort => sort}
49
+ end
50
+
51
+ # @return [Array<Hash>] Mongo criteria and options enclosed in an Array
52
+ def to_a
53
+ [criteria, options]
54
+ end
55
+
56
+ private
57
+ def to_mongo_criteria(conditions, parent_key=nil)
58
+ criteria = {}
59
+
60
+ conditions.each_pair do |field, value|
61
+ field = normalized_field(field)
62
+
63
+ if @model.object_id_key?(field) && value.is_a?(String)
64
+ value = Mongo::ObjectID.from_string(value)
65
+ end
66
+
67
+ if field.is_a?(FinderOperator)
68
+ criteria.merge!(field.to_criteria(value))
69
+ next
70
+ end
71
+
72
+ case value
73
+ when Array
74
+ criteria[field] = operator?(field) ? value : {'$in' => value}
75
+ when Hash
76
+ criteria[field] = to_mongo_criteria(value, field)
77
+ else
78
+ criteria[field] = value
79
+ end
80
+ end
81
+
82
+ criteria
83
+ end
84
+
85
+ def operator?(field)
86
+ field.to_s =~ /^\$/
87
+ end
88
+
89
+ def normalized_field(field)
90
+ field.to_s == 'id' ? :_id : field
91
+ end
92
+
93
+ # adds _type single collection inheritance scope for models that need it
94
+ def add_sci_scope
95
+ if @model.single_collection_inherited?
96
+ @conditions[:_type] = @model.to_s
97
+ end
98
+ end
99
+
100
+ def to_mongo_fields(fields)
101
+ return if fields.blank?
102
+
103
+ if fields.is_a?(String)
104
+ fields.split(',').map { |field| field.strip }
105
+ else
106
+ fields.flatten.compact
107
+ end
108
+ end
109
+
110
+ def convert_order_to_sort(sort)
111
+ return if sort.blank?
112
+ pieces = sort.split(',')
113
+ pieces.map { |s| to_mongo_sort_piece(s) }
114
+ end
115
+
116
+ def to_mongo_sort_piece(str)
117
+ field, direction = str.strip.split(' ')
118
+ direction ||= 'ASC'
119
+ direction = direction.upcase == 'ASC' ? 1 : -1
120
+ [field, direction]
121
+ end
122
+ end
123
+
124
+ class FinderOperator
125
+ def initialize(field, operator)
126
+ @field, @operator = field, operator
127
+ end
128
+
129
+ def to_criteria(value)
130
+ {@field => {@operator => value}}
131
+ end
132
+ end
133
+ end