danielharan-mongo_mapper 0.6.5

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