djsun-mongo_mapper 0.5.0.1

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 (71) hide show
  1. data/.gitignore +8 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +39 -0
  4. data/Rakefile +87 -0
  5. data/VERSION +1 -0
  6. data/bin/mmconsole +55 -0
  7. data/lib/mongo_mapper/associations/base.rb +83 -0
  8. data/lib/mongo_mapper/associations/belongs_to_polymorphic_proxy.rb +34 -0
  9. data/lib/mongo_mapper/associations/belongs_to_proxy.rb +22 -0
  10. data/lib/mongo_mapper/associations/many_documents_as_proxy.rb +27 -0
  11. data/lib/mongo_mapper/associations/many_documents_proxy.rb +116 -0
  12. data/lib/mongo_mapper/associations/many_embedded_polymorphic_proxy.rb +33 -0
  13. data/lib/mongo_mapper/associations/many_embedded_proxy.rb +67 -0
  14. data/lib/mongo_mapper/associations/many_polymorphic_proxy.rb +11 -0
  15. data/lib/mongo_mapper/associations/many_proxy.rb +6 -0
  16. data/lib/mongo_mapper/associations/proxy.rb +74 -0
  17. data/lib/mongo_mapper/associations.rb +86 -0
  18. data/lib/mongo_mapper/callbacks.rb +106 -0
  19. data/lib/mongo_mapper/document.rb +308 -0
  20. data/lib/mongo_mapper/dynamic_finder.rb +35 -0
  21. data/lib/mongo_mapper/embedded_document.rb +354 -0
  22. data/lib/mongo_mapper/finder_options.rb +94 -0
  23. data/lib/mongo_mapper/key.rb +32 -0
  24. data/lib/mongo_mapper/observing.rb +50 -0
  25. data/lib/mongo_mapper/pagination.rb +51 -0
  26. data/lib/mongo_mapper/rails_compatibility/document.rb +15 -0
  27. data/lib/mongo_mapper/rails_compatibility/embedded_document.rb +27 -0
  28. data/lib/mongo_mapper/save_with_validation.rb +19 -0
  29. data/lib/mongo_mapper/serialization.rb +55 -0
  30. data/lib/mongo_mapper/serializers/json_serializer.rb +92 -0
  31. data/lib/mongo_mapper/support.rb +171 -0
  32. data/lib/mongo_mapper/validations.rb +69 -0
  33. data/lib/mongo_mapper.rb +95 -0
  34. data/mongo_mapper.gemspec +156 -0
  35. data/specs.watchr +32 -0
  36. data/test/NOTE_ON_TESTING +1 -0
  37. data/test/custom_matchers.rb +48 -0
  38. data/test/functional/associations/test_belongs_to_polymorphic_proxy.rb +55 -0
  39. data/test/functional/associations/test_belongs_to_proxy.rb +49 -0
  40. data/test/functional/associations/test_many_documents_as_proxy.rb +244 -0
  41. data/test/functional/associations/test_many_embedded_polymorphic_proxy.rb +132 -0
  42. data/test/functional/associations/test_many_embedded_proxy.rb +174 -0
  43. data/test/functional/associations/test_many_polymorphic_proxy.rb +297 -0
  44. data/test/functional/associations/test_many_proxy.rb +331 -0
  45. data/test/functional/test_associations.rb +44 -0
  46. data/test/functional/test_binary.rb +18 -0
  47. data/test/functional/test_callbacks.rb +85 -0
  48. data/test/functional/test_document.rb +964 -0
  49. data/test/functional/test_embedded_document.rb +97 -0
  50. data/test/functional/test_logger.rb +20 -0
  51. data/test/functional/test_pagination.rb +87 -0
  52. data/test/functional/test_rails_compatibility.rb +30 -0
  53. data/test/functional/test_validations.rb +279 -0
  54. data/test/models.rb +169 -0
  55. data/test/test_helper.rb +30 -0
  56. data/test/unit/serializers/test_json_serializer.rb +193 -0
  57. data/test/unit/test_association_base.rb +144 -0
  58. data/test/unit/test_document.rb +165 -0
  59. data/test/unit/test_dynamic_finder.rb +125 -0
  60. data/test/unit/test_embedded_document.rb +643 -0
  61. data/test/unit/test_finder_options.rb +193 -0
  62. data/test/unit/test_key.rb +175 -0
  63. data/test/unit/test_mongomapper.rb +28 -0
  64. data/test/unit/test_observing.rb +101 -0
  65. data/test/unit/test_pagination.rb +109 -0
  66. data/test/unit/test_rails_compatibility.rb +39 -0
  67. data/test/unit/test_serializations.rb +52 -0
  68. data/test/unit/test_support.rb +272 -0
  69. data/test/unit/test_time_zones.rb +40 -0
  70. data/test/unit/test_validations.rb +503 -0
  71. metadata +207 -0
@@ -0,0 +1,354 @@
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, String
20
+ attr_accessor :_root_document
21
+ end
22
+ end
23
+
24
+ module ClassMethods
25
+ def inherited(subclass)
26
+ unless subclass.embeddable?
27
+ subclass.set_collection_name(collection_name)
28
+ end
29
+
30
+ (@subclasses ||= []) << subclass
31
+ end
32
+
33
+ def subclasses
34
+ @subclasses
35
+ end
36
+
37
+ def keys
38
+ @keys ||= if parent = parent_model
39
+ parent.keys.dup
40
+ else
41
+ HashWithIndifferentAccess.new
42
+ end
43
+ end
44
+
45
+ def key(*args)
46
+ key = Key.new(*args)
47
+
48
+ if keys[key.name].blank?
49
+ keys[key.name] = key
50
+
51
+ create_accessors_for(key)
52
+ add_to_subclasses(*args)
53
+ apply_validations_for(key)
54
+ create_indexes_for(key)
55
+
56
+ key
57
+ end
58
+ end
59
+
60
+ def add_to_subclasses(*args)
61
+ return if subclasses.blank?
62
+
63
+ subclasses.each do |subclass|
64
+ subclass.key(*args)
65
+ end
66
+ end
67
+
68
+ def ensure_index(name_or_array, options={})
69
+ keys_to_index = if name_or_array.is_a?(Array)
70
+ name_or_array.map { |pair| [pair[0], pair[1]] }
71
+ else
72
+ name_or_array
73
+ end
74
+
75
+ collection.create_index(keys_to_index, options.delete(:unique))
76
+ end
77
+
78
+ def embeddable?
79
+ !self.ancestors.include?(Document)
80
+ end
81
+
82
+ def parent_model
83
+ (ancestors - [self,EmbeddedDocument]).find do |parent_class|
84
+ parent_class.ancestors.include?(EmbeddedDocument)
85
+ end
86
+ end
87
+
88
+ def to_mongo(instance)
89
+ return nil if instance.nil?
90
+ instance.to_mongo
91
+ end
92
+
93
+ def from_mongo(instance_or_hash)
94
+ return nil if instance_or_hash.nil?
95
+
96
+ if instance_or_hash.is_a?(self)
97
+ instance_or_hash
98
+ else
99
+ new(instance_or_hash)
100
+ end
101
+ end
102
+
103
+ private
104
+ def accessors_module
105
+ if const_defined?('MongoMapperKeys')
106
+ const_get 'MongoMapperKeys'
107
+ else
108
+ const_set 'MongoMapperKeys', Module.new
109
+ end
110
+ end
111
+
112
+ def create_accessors_for(key)
113
+ accessors_module.module_eval <<-end_eval
114
+ def #{key.name}
115
+ read_attribute(:'#{key.name}')
116
+ end
117
+
118
+ def #{key.name}_before_typecast
119
+ read_attribute_before_typecast(:'#{key.name}')
120
+ end
121
+
122
+ def #{key.name}=(value)
123
+ write_attribute(:'#{key.name}', value)
124
+ end
125
+
126
+ def #{key.name}?
127
+ read_attribute(:#{key.name}).present?
128
+ end
129
+ end_eval
130
+ include accessors_module
131
+ end
132
+
133
+ def create_indexes_for(key)
134
+ ensure_index key.name if key.options[:index]
135
+ end
136
+
137
+ def apply_validations_for(key)
138
+ attribute = key.name.to_sym
139
+
140
+ if key.options[:required]
141
+ validates_presence_of(attribute)
142
+ end
143
+
144
+ if key.options[:unique]
145
+ validates_uniqueness_of(attribute)
146
+ end
147
+
148
+ if key.options[:numeric]
149
+ number_options = key.type == Integer ? {:only_integer => true} : {}
150
+ validates_numericality_of(attribute, number_options)
151
+ end
152
+
153
+ if key.options[:format]
154
+ validates_format_of(attribute, :with => key.options[:format])
155
+ end
156
+
157
+ if key.options[:length]
158
+ length_options = case key.options[:length]
159
+ when Integer
160
+ {:minimum => 0, :maximum => key.options[:length]}
161
+ when Range
162
+ {:within => key.options[:length]}
163
+ when Hash
164
+ key.options[:length]
165
+ end
166
+ validates_length_of(attribute, length_options)
167
+ end
168
+ end
169
+ end
170
+
171
+ module InstanceMethods
172
+ def initialize(attrs={})
173
+ unless attrs.nil?
174
+ self.class.associations.each_pair do |name, association|
175
+ if collection = attrs.delete(name)
176
+ if association.many? && association.klass.embeddable?
177
+ root_document = attrs[:_root_document] || self
178
+ collection.each do |doc|
179
+ doc[:_root_document] = root_document
180
+ end
181
+ end
182
+ send("#{association.name}=", collection)
183
+ end
184
+ end
185
+
186
+ self.attributes = attrs
187
+
188
+ if respond_to?(:_type=) && self['_type'].blank?
189
+ self._type = self.class.name
190
+ end
191
+ end
192
+
193
+ if self.class.embeddable?
194
+ if read_attribute(:_id).blank?
195
+ write_attribute :_id, Mongo::ObjectID.new.to_s
196
+ @new_document = true
197
+ else
198
+ @new_document = false
199
+ end
200
+ end
201
+ end
202
+
203
+ def new?
204
+ !!@new_document
205
+ end
206
+
207
+ def attributes=(attrs)
208
+ return if attrs.blank?
209
+ attrs.each_pair do |name, value|
210
+ writer_method = "#{name}="
211
+
212
+ if respond_to?(writer_method)
213
+ self.send(writer_method, value)
214
+ else
215
+ self[name.to_s] = value
216
+ end
217
+ end
218
+ end
219
+
220
+ def attributes
221
+ attrs = HashWithIndifferentAccess.new
222
+
223
+ embedded_keys.each do |key|
224
+ puts key.inspect
225
+ attrs[key.name] = read_attribute(key.name).try(:attributes)
226
+ end
227
+
228
+ non_embedded_keys.each do |key|
229
+ attrs[key.name] = read_attribute(key.name)
230
+ end
231
+
232
+ embedded_associations.each do |association|
233
+ documents = instance_variable_get(association.ivar)
234
+ next if documents.nil?
235
+ attrs[association.name] = documents.collect { |doc| doc.attributes }
236
+ end
237
+
238
+ attrs
239
+ end
240
+
241
+ def to_mongo
242
+ attrs = HashWithIndifferentAccess.new
243
+
244
+ _keys.each_pair do |name, key|
245
+ value = key.set(read_attribute(key.name))
246
+ attrs[name] = value
247
+ end
248
+
249
+ embedded_associations.each do |association|
250
+ if documents = instance_variable_get(association.ivar)
251
+ attrs[association.name] = documents.map { |document| document.to_mongo }
252
+ end
253
+ end
254
+
255
+ attrs
256
+ end
257
+
258
+ def clone
259
+ clone_attributes = self.attributes
260
+ clone_attributes.delete("_id")
261
+ self.class.new(clone_attributes)
262
+ end
263
+
264
+ def [](name)
265
+ read_attribute(name)
266
+ end
267
+
268
+ def []=(name, value)
269
+ ensure_key_exists(name)
270
+ write_attribute(name, value)
271
+ end
272
+
273
+ def ==(other)
274
+ other.is_a?(self.class) && id == other.id
275
+ end
276
+
277
+ def id
278
+ read_attribute(:_id)
279
+ end
280
+
281
+ def id=(value)
282
+ @using_custom_id = true
283
+ write_attribute :_id, value
284
+ end
285
+
286
+ def using_custom_id?
287
+ !!@using_custom_id
288
+ end
289
+
290
+ def inspect
291
+ attributes_as_nice_string = key_names.collect do |name|
292
+ "#{name}: #{read_attribute(name).inspect}"
293
+ end.join(", ")
294
+ "#<#{self.class} #{attributes_as_nice_string}>"
295
+ end
296
+
297
+ def save
298
+ if _root_document
299
+ _root_document.save
300
+ end
301
+ end
302
+
303
+ def update_attributes(attrs={})
304
+ self.attributes = attrs
305
+ save
306
+ end
307
+
308
+ private
309
+ def _keys
310
+ self.class.keys
311
+ end
312
+
313
+ def key_names
314
+ _keys.keys
315
+ end
316
+
317
+ def non_embedded_keys
318
+ _keys.values.select { |key| !key.embeddable? }
319
+ end
320
+
321
+ def embedded_keys
322
+ _keys.values.select { |key| key.embeddable? }
323
+ end
324
+
325
+ def ensure_key_exists(name)
326
+ self.class.key(name) unless respond_to?("#{name}=")
327
+ end
328
+
329
+ def read_attribute(name)
330
+ value = _keys[name].get(instance_variable_get("@#{name}"))
331
+ instance_variable_set "@#{name}", value if !frozen?
332
+ value
333
+ end
334
+
335
+ def read_attribute_before_typecast(name)
336
+ instance_variable_get("@#{name}_before_typecast")
337
+ end
338
+
339
+ def write_attribute(name, value)
340
+ key = _keys[name]
341
+ instance_variable_set "@#{name}_before_typecast", value
342
+ instance_variable_set "@#{name}", key.set(value)
343
+ end
344
+
345
+ def embedded_associations
346
+ self.class.associations.select do |name, association|
347
+ association.embeddable?
348
+ end.map do |name, association|
349
+ association
350
+ end
351
+ end
352
+ end # InstanceMethods
353
+ end # EmbeddedDocument
354
+ end # MongoMapper
@@ -0,0 +1,94 @@
1
+ module MongoMapper
2
+ class FinderOptions
3
+ attr_reader :options
4
+
5
+ def self.to_mongo_criteria(conditions, parent_key=nil)
6
+ criteria = {}
7
+ conditions.each_pair do |field, value|
8
+ field = field_normalized(field)
9
+ case value
10
+ when Array
11
+ operator_present = field.to_s =~ /^\$/
12
+ criteria[field] = if operator_present
13
+ value
14
+ else
15
+ {'$in' => value}
16
+ end
17
+ when Hash
18
+ criteria[field] = to_mongo_criteria(value, field)
19
+ else
20
+ criteria[field] = value
21
+ end
22
+ end
23
+
24
+ criteria
25
+ end
26
+
27
+ def self.to_mongo_options(options)
28
+ options = options.dup
29
+ {
30
+ :fields => to_mongo_fields(options.delete(:fields) || options.delete(:select)),
31
+ :skip => (options.delete(:skip) || options.delete(:offset) || 0).to_i,
32
+ :limit => (options.delete(:limit) || 0).to_i,
33
+ :sort => options.delete(:sort) || to_mongo_sort(options.delete(:order))
34
+ }
35
+ end
36
+
37
+ def self.field_normalized(field)
38
+ if field.to_s == 'id'
39
+ :_id
40
+ else
41
+ field
42
+ end
43
+ end
44
+
45
+ def initialize(options)
46
+ raise ArgumentError, "FinderOptions must be a hash" unless options.is_a?(Hash)
47
+ @options = options.symbolize_keys
48
+ @conditions = @options.delete(:conditions) || {}
49
+ end
50
+
51
+ def criteria
52
+ self.class.to_mongo_criteria(@conditions)
53
+ end
54
+
55
+ def options
56
+ self.class.to_mongo_options(@options)
57
+ end
58
+
59
+ def to_a
60
+ [criteria, options]
61
+ end
62
+
63
+ private
64
+ def self.to_mongo_fields(fields)
65
+ return if fields.blank?
66
+
67
+ if fields.is_a?(String)
68
+ fields.split(',').map { |field| field.strip }
69
+ else
70
+ fields.flatten.compact
71
+ end
72
+ end
73
+
74
+ def self.to_mongo_sort(sort)
75
+ return if sort.blank?
76
+ pieces = sort.split(',')
77
+ pairs = pieces.map { |s| to_mongo_sort_piece(s) }
78
+
79
+ hash = OrderedHash.new
80
+ pairs.each do |pair|
81
+ field, sort_direction = pair
82
+ hash[field] = sort_direction
83
+ end
84
+ hash.symbolize_keys
85
+ end
86
+
87
+ def self.to_mongo_sort_piece(str)
88
+ field, direction = str.strip.split(' ')
89
+ direction ||= 'ASC'
90
+ direction = direction.upcase == 'ASC' ? 1 : -1
91
+ [field, direction]
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,32 @@
1
+ module MongoMapper
2
+ class Key
3
+ attr_accessor :name, :type, :options, :default_value
4
+
5
+ def initialize(*args)
6
+ options = args.extract_options!
7
+ @name, @type = args.shift.to_s, args.shift
8
+ self.options = (options || {}).symbolize_keys
9
+ self.default_value = self.options.delete(:default)
10
+ end
11
+
12
+ def ==(other)
13
+ @name == other.name && @type == other.type
14
+ end
15
+
16
+ def set(value)
17
+ type.to_mongo(value)
18
+ end
19
+
20
+ def embeddable?
21
+ type.respond_to?(:embeddable?) && type.embeddable? ? true : false
22
+ end
23
+
24
+ def get(value)
25
+ if value.nil? && !default_value.nil?
26
+ return default_value
27
+ end
28
+
29
+ type.from_mongo(value)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,50 @@
1
+ require 'observer'
2
+ require 'singleton'
3
+ require 'set'
4
+
5
+ module MongoMapper
6
+ module Observing #:nodoc:
7
+ def self.included(model)
8
+ model.class_eval do
9
+ extend Observable
10
+ end
11
+ end
12
+ end
13
+
14
+ class Observer
15
+ include Singleton
16
+
17
+ class << self
18
+ def observe(*models)
19
+ models.flatten!
20
+ models.collect! { |model| model.is_a?(Symbol) ? model.to_s.camelize.constantize : model }
21
+ define_method(:observed_classes) { Set.new(models) }
22
+ end
23
+
24
+ def observed_class
25
+ if observed_class_name = name[/(.*)Observer/, 1]
26
+ observed_class_name.constantize
27
+ else
28
+ nil
29
+ end
30
+ end
31
+ end
32
+
33
+ def initialize
34
+ Set.new(observed_classes).each { |klass| add_observer! klass }
35
+ end
36
+
37
+ def update(observed_method, object) #:nodoc:
38
+ send(observed_method, object) if respond_to?(observed_method)
39
+ end
40
+
41
+ protected
42
+ def observed_classes
43
+ Set.new([self.class.observed_class].compact.flatten)
44
+ end
45
+
46
+ def add_observer!(klass)
47
+ klass.add_observer(self)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,51 @@
1
+ module MongoMapper
2
+ module Pagination
3
+ class PaginationProxy < BasicObject
4
+ attr_accessor :subject
5
+ attr_reader :total_entries, :per_page, :current_page
6
+ alias limit per_page
7
+
8
+ def initialize(total_entries, current_page, per_page=nil)
9
+ @total_entries = total_entries.to_i
10
+ self.per_page = per_page
11
+ self.current_page = current_page
12
+ end
13
+
14
+ def total_pages
15
+ (total_entries / per_page.to_f).ceil
16
+ end
17
+
18
+ def out_of_bounds?
19
+ current_page > total_pages
20
+ end
21
+
22
+ def previous_page
23
+ current_page > 1 ? (current_page - 1) : nil
24
+ end
25
+
26
+ def next_page
27
+ current_page < total_pages ? (current_page + 1) : nil
28
+ end
29
+
30
+ def skip
31
+ (current_page - 1) * per_page
32
+ end
33
+
34
+ def method_missing(name, *args, &block)
35
+ @subject.send(name, *args, &block)
36
+ end
37
+
38
+ private
39
+ def per_page=(value)
40
+ value = 25 if value.blank?
41
+ @per_page = value.to_i
42
+ end
43
+
44
+ def current_page=(value)
45
+ value = value.to_i
46
+ value = 1 if value < 1
47
+ @current_page = value
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,15 @@
1
+ module MongoMapper
2
+ module RailsCompatibility
3
+ module Document
4
+ def self.included(model)
5
+ model.class_eval do
6
+ alias_method :new_record?, :new?
7
+ end
8
+ end
9
+
10
+ def to_param
11
+ id
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ module MongoMapper
2
+ module RailsCompatibility
3
+ module EmbeddedDocument
4
+ def self.included(model)
5
+ model.class_eval do
6
+ extend ClassMethods
7
+
8
+ alias_method :new_record?, :new?
9
+ end
10
+
11
+ class << model
12
+ alias has_many many
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ def column_names
18
+ keys.keys
19
+ end
20
+ end
21
+
22
+ def to_param
23
+ raise "Missing to_param method in #{self.class}. You should implement it to return the unique identifier of this embedded document within a document."
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ module MongoMapper
2
+ module SaveWithValidation
3
+ def self.included(base)
4
+ base.class_eval do
5
+ alias_method_chain :save, :validation
6
+ alias_method_chain :save!, :validation
7
+ end
8
+ end
9
+
10
+ private
11
+ def save_with_validation
12
+ valid? ? save_without_validation : false
13
+ end
14
+
15
+ def save_with_validation!
16
+ valid? ? save_without_validation! : raise(DocumentNotValid.new(self))
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,55 @@
1
+ require 'active_support/json'
2
+
3
+ module MongoMapper #:nodoc:
4
+ module Serialization
5
+ class Serializer #:nodoc:
6
+ attr_reader :options
7
+
8
+ def initialize(record, options = {})
9
+ @record, @options = record, options.dup
10
+ end
11
+
12
+ def serializable_key_names
13
+ key_names = @record.attributes.keys
14
+
15
+ if options[:only]
16
+ options.delete(:except)
17
+ key_names = key_names & Array(options[:only]).collect { |n| n.to_s }
18
+ else
19
+ options[:except] = Array(options[:except])
20
+ key_names = key_names - options[:except].collect { |n| n.to_s }
21
+ end
22
+
23
+ key_names
24
+ end
25
+
26
+ def serializable_method_names
27
+ Array(options[:methods]).inject([]) do |method_attributes, name|
28
+ method_attributes << name if @record.respond_to?(name.to_s)
29
+ method_attributes
30
+ end
31
+ end
32
+
33
+ def serializable_names
34
+ serializable_key_names + serializable_method_names
35
+ end
36
+
37
+ def serializable_record
38
+ returning(serializable_record = {}) do
39
+ serializable_names.each { |name| serializable_record[name] = @record.send(name) }
40
+ end
41
+ end
42
+
43
+ def serialize
44
+ # overwrite to implement
45
+ end
46
+
47
+ def to_s(&block)
48
+ serialize(&block)
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ dir = Pathname(__FILE__).dirname.expand_path + 'serializers'
55
+ require dir + 'json_serializer'