ramsingla-mongomapper 0.2.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 (42) hide show
  1. data/.gitignore +7 -0
  2. data/History +30 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +39 -0
  5. data/Rakefile +71 -0
  6. data/VERSION +1 -0
  7. data/lib/mongomapper.rb +60 -0
  8. data/lib/mongomapper/associations.rb +69 -0
  9. data/lib/mongomapper/associations/array_proxy.rb +6 -0
  10. data/lib/mongomapper/associations/base.rb +50 -0
  11. data/lib/mongomapper/associations/belongs_to_proxy.rb +26 -0
  12. data/lib/mongomapper/associations/has_many_embedded_proxy.rb +19 -0
  13. data/lib/mongomapper/associations/has_many_proxy.rb +28 -0
  14. data/lib/mongomapper/associations/polymorphic_belongs_to_proxy.rb +31 -0
  15. data/lib/mongomapper/associations/proxy.rb +60 -0
  16. data/lib/mongomapper/callbacks.rb +106 -0
  17. data/lib/mongomapper/document.rb +263 -0
  18. data/lib/mongomapper/embedded_document.rb +295 -0
  19. data/lib/mongomapper/finder_options.rb +81 -0
  20. data/lib/mongomapper/key.rb +82 -0
  21. data/lib/mongomapper/observing.rb +50 -0
  22. data/lib/mongomapper/pagination.rb +52 -0
  23. data/lib/mongomapper/rails_compatibility.rb +23 -0
  24. data/lib/mongomapper/save_with_validation.rb +19 -0
  25. data/lib/mongomapper/serialization.rb +55 -0
  26. data/lib/mongomapper/serializers/json_serializer.rb +77 -0
  27. data/lib/mongomapper/validations.rb +47 -0
  28. data/mongomapper.gemspec +105 -0
  29. data/test/serializers/test_json_serializer.rb +104 -0
  30. data/test/test_associations.rb +211 -0
  31. data/test/test_callbacks.rb +84 -0
  32. data/test/test_document.rb +995 -0
  33. data/test/test_embedded_document.rb +253 -0
  34. data/test/test_finder_options.rb +148 -0
  35. data/test/test_helper.rb +62 -0
  36. data/test/test_key.rb +200 -0
  37. data/test/test_mongomapper.rb +28 -0
  38. data/test/test_observing.rb +101 -0
  39. data/test/test_rails_compatibility.rb +29 -0
  40. data/test/test_serializations.rb +54 -0
  41. data/test/test_validations.rb +409 -0
  42. metadata +156 -0
@@ -0,0 +1,60 @@
1
+ module MongoMapper
2
+ module Associations
3
+ class Proxy
4
+ attr_reader :owner, :association
5
+
6
+ instance_methods.each do |m|
7
+ undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/
8
+ end
9
+
10
+ def initialize(owner, association)
11
+ @owner= owner
12
+ @association = association
13
+
14
+ reset
15
+ end
16
+
17
+ def respond_to?(*methods)
18
+ (load_target && @target.respond_to?(*methods))
19
+ end
20
+
21
+ def reset
22
+ @target = nil
23
+ end
24
+
25
+ def reload_target
26
+ reset
27
+ load_target
28
+ self
29
+ end
30
+
31
+ def send(method, *args)
32
+ load_target
33
+ @target.send(method, *args)
34
+ end
35
+
36
+ def replace(v)
37
+ raise NotImplementedError
38
+ end
39
+
40
+ protected
41
+ def method_missing(method, *args)
42
+ if load_target
43
+ if block_given?
44
+ @target.send(method, *args) { |*block_args| yield(*block_args) }
45
+ else
46
+ @target.send(method, *args)
47
+ end
48
+ end
49
+ end
50
+
51
+ def load_target
52
+ @target ||= find_target
53
+ end
54
+
55
+ def find_target
56
+ raise NotImplementedError
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,106 @@
1
+ module MongoMapper
2
+ module Callbacks
3
+ def self.included(model) #:nodoc:
4
+ model.class_eval do
5
+ extend Observable
6
+ include ActiveSupport::Callbacks
7
+
8
+ define_callbacks *%w(
9
+ before_save after_save before_create after_create before_update after_update before_validation
10
+ after_validation before_validation_on_create after_validation_on_create before_validation_on_update
11
+ after_validation_on_update before_destroy after_destroy
12
+ )
13
+
14
+ [:create_or_update, :valid?, :create, :update, :destroy].each do |method|
15
+ alias_method_chain method, :callbacks
16
+ end
17
+ end
18
+ end
19
+
20
+ def before_save() end
21
+
22
+ def after_save() end
23
+ def create_or_update_with_callbacks #:nodoc:
24
+ return false if callback(:before_save) == false
25
+ if result = create_or_update_without_callbacks
26
+ callback(:after_save)
27
+ end
28
+ result
29
+ end
30
+ private :create_or_update_with_callbacks
31
+
32
+ def before_create() end
33
+
34
+ def after_create() end
35
+ def create_with_callbacks #:nodoc:
36
+ return false if callback(:before_create) == false
37
+ result = create_without_callbacks
38
+ callback(:after_create)
39
+ result
40
+ end
41
+ private :create_with_callbacks
42
+
43
+ def before_update() end
44
+
45
+ def after_update() end
46
+
47
+ def update_with_callbacks(*args) #:nodoc:
48
+ return false if callback(:before_update) == false
49
+ result = update_without_callbacks(*args)
50
+ callback(:after_update)
51
+ result
52
+ end
53
+ private :update_with_callbacks
54
+
55
+ def before_validation() end
56
+
57
+ def after_validation() end
58
+
59
+ def before_validation_on_create() end
60
+
61
+ def after_validation_on_create() end
62
+
63
+ def before_validation_on_update() end
64
+
65
+ def after_validation_on_update() end
66
+
67
+ def valid_with_callbacks? #:nodoc:
68
+ return false if callback(:before_validation) == false
69
+ result = new? ? callback(:before_validation_on_create) : callback(:before_validation_on_update)
70
+ return false if false == result
71
+
72
+ result = valid_without_callbacks?
73
+ callback(:after_validation)
74
+
75
+ new? ? callback(:after_validation_on_create) : callback(:after_validation_on_update)
76
+ return result
77
+ end
78
+
79
+ def before_destroy() end
80
+
81
+ def after_destroy() end
82
+ def destroy_with_callbacks #:nodoc:
83
+ return false if callback(:before_destroy) == false
84
+ result = destroy_without_callbacks
85
+ callback(:after_destroy)
86
+ result
87
+ end
88
+
89
+ private
90
+ def callback(method)
91
+ result = run_callbacks(method) { |result, object| false == result }
92
+
93
+ if result != false && respond_to_without_attributes?(method)
94
+ result = send(method)
95
+ end
96
+
97
+ notify(method)
98
+ return result
99
+ end
100
+
101
+ def notify(method) #:nodoc:
102
+ self.class.changed
103
+ self.class.notify_observers(method, self)
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,263 @@
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 SaveWithValidation
12
+ extend ClassMethods
13
+
14
+ key :_id, String
15
+ key :created_at, Time
16
+ key :updated_at, Time
17
+ end
18
+
19
+ descendants << model
20
+ end
21
+
22
+ def self.descendants
23
+ @descendants ||= Set.new
24
+ end
25
+
26
+ module ClassMethods
27
+ def find(*args)
28
+ options = args.extract_options!
29
+
30
+ case args.first
31
+ when :first then find_first(options)
32
+ when :last then find_last(options)
33
+ when :all then find_every(options)
34
+ else find_from_ids(args)
35
+ end
36
+ end
37
+
38
+ def paginate(options)
39
+ per_page = options.delete(:per_page)
40
+ page = options.delete(:page)
41
+ total_entries = count(options)
42
+
43
+ collection = Pagination::PaginationProxy.new(total_entries, page, per_page)
44
+
45
+ options[:limit] = collection.limit
46
+ options[:skip] = collection.skip
47
+
48
+ collection.subject = find_every(options)
49
+ collection
50
+ end
51
+
52
+ def first(options={})
53
+ find_first(options)
54
+ end
55
+
56
+ def last(options={})
57
+ find_last(options)
58
+ end
59
+
60
+ def all(options={})
61
+ find_every(options)
62
+ end
63
+
64
+ def find_by_id(id)
65
+ if doc = collection.find_first({:_id => id})
66
+ new(doc)
67
+ end
68
+ end
69
+
70
+ # TODO: remove the rescuing when ruby driver works correctly
71
+ def count(conditions={})
72
+ collection.count(FinderOptions.to_mongo_criteria(conditions))
73
+ end
74
+
75
+ def create(*docs)
76
+ instances = []
77
+ docs.flatten.each do |attrs|
78
+ doc = new(attrs); doc.save
79
+ instances << doc
80
+ end
81
+ instances.size == 1 ? instances[0] : instances
82
+ end
83
+
84
+ # For updating single document
85
+ # Person.update(1, {:foo => 'bar'})
86
+ #
87
+ # For updating multiple documents at once:
88
+ # Person.update({'1' => {:foo => 'bar'}, '2' => {:baz => 'wick'}})
89
+ def update(*args)
90
+ updating_multiple = args.length == 1
91
+ if updating_multiple
92
+ update_multiple(args[0])
93
+ else
94
+ id, attributes = args
95
+ update_single(id, attributes)
96
+ end
97
+ end
98
+
99
+ def delete(*ids)
100
+ collection.remove(:_id => {'$in' => ids.flatten})
101
+ end
102
+
103
+ def delete_all(conditions={})
104
+ collection.remove(FinderOptions.to_mongo_criteria(conditions))
105
+ end
106
+
107
+ def destroy(*ids)
108
+ find_some(ids.flatten).each(&:destroy)
109
+ end
110
+
111
+ def destroy_all(conditions={})
112
+ find(:all, :conditions => conditions).each(&:destroy)
113
+ end
114
+
115
+ def connection(mongo_connection=nil)
116
+ if mongo_connection.nil?
117
+ @connection ||= MongoMapper.connection
118
+ else
119
+ @connection = mongo_connection
120
+ end
121
+ @connection
122
+ end
123
+
124
+ def database(name=nil)
125
+ if name.nil?
126
+ @database ||= MongoMapper.database
127
+ else
128
+ @database = connection.db(name)
129
+ end
130
+ @database
131
+ end
132
+
133
+ def collection(name=nil)
134
+ if name.nil?
135
+ @collection ||= database.collection(self.to_s.demodulize.tableize)
136
+ else
137
+ @collection = database.collection(name)
138
+ end
139
+ @collection
140
+ end
141
+
142
+ private
143
+ def find_every(options)
144
+ criteria, options = FinderOptions.new(options).to_a
145
+ collection.find(criteria, options).to_a.map { |doc| new(doc) }
146
+ end
147
+
148
+ def find_first(options)
149
+ find_every(options.merge(:limit => 1, :order => 'created_at')).first
150
+ end
151
+
152
+ def find_last(options)
153
+ find_every(options.merge(:limit => 1, :order => 'created_at desc')).first
154
+ end
155
+
156
+ def find_some(ids)
157
+ documents = find_every(:conditions => {'_id' => ids})
158
+ if ids.size == documents.size
159
+ documents
160
+ else
161
+ raise DocumentNotFound, "Couldn't find all of the ids (#{ids.to_sentence}). Found #{documents.size}, but was expecting #{ids.size}"
162
+ end
163
+ end
164
+
165
+ def find_from_ids(*ids)
166
+ ids = ids.flatten.compact.uniq
167
+
168
+ case ids.size
169
+ when 0
170
+ raise(DocumentNotFound, "Couldn't find without an ID")
171
+ when 1
172
+ find_by_id(ids[0]) || raise(DocumentNotFound, "Document with id of #{ids[0]} does not exist in collection named #{collection.name}")
173
+ else
174
+ find_some(ids)
175
+ end
176
+ end
177
+
178
+ def update_single(id, attrs)
179
+ if id.blank? || attrs.blank? || !attrs.is_a?(Hash)
180
+ raise ArgumentError, "Updating a single document requires an id and a hash of attributes"
181
+ end
182
+
183
+ find(id).update_attributes(attrs)
184
+ end
185
+
186
+ def update_multiple(docs)
187
+ unless docs.is_a?(Hash)
188
+ raise ArgumentError, "Updating multiple documents takes 1 argument and it must be hash"
189
+ end
190
+ instances = []
191
+ docs.each_pair { |id, attrs| instances << update(id, attrs) }
192
+ instances
193
+ end
194
+ end
195
+
196
+ module InstanceMethods
197
+ def collection
198
+ self.class.collection
199
+ end
200
+
201
+ def new?
202
+ read_attribute('_id').blank? || self.class.find_by_id(id).blank?
203
+ end
204
+
205
+ def save
206
+ create_or_update
207
+ end
208
+
209
+ def save!
210
+ create_or_update || raise(DocumentNotValid.new(self))
211
+ end
212
+
213
+ def update_attributes(attrs={})
214
+ self.attributes = attrs
215
+ save
216
+ self
217
+ end
218
+
219
+ def destroy
220
+ collection.remove(:_id => id) unless new?
221
+ freeze
222
+ end
223
+
224
+ def ==(other)
225
+ other.is_a?(self.class) && id == other.id
226
+ end
227
+
228
+ def id
229
+ read_attribute('_id')
230
+ end
231
+
232
+ private
233
+ def create_or_update
234
+ result = new? ? create : update
235
+ result != false
236
+ end
237
+
238
+ def create
239
+ write_attribute('_id', generate_id) if read_attribute('_id').blank?
240
+ update_timestamps
241
+ save_to_collection
242
+ end
243
+
244
+ def update
245
+ update_timestamps
246
+ save_to_collection
247
+ end
248
+
249
+ def save_to_collection
250
+ collection.save(attributes.merge!(embedded_association_attributes))
251
+ end
252
+
253
+ def update_timestamps
254
+ write_attribute('created_at', Time.now.utc) if new?
255
+ write_attribute('updated_at', Time.now.utc)
256
+ end
257
+
258
+ def generate_id
259
+ XGen::Mongo::Driver::ObjectID.new
260
+ end
261
+ end
262
+ end # Document
263
+ end # MongoMapper
@@ -0,0 +1,295 @@
1
+ require 'observer'
2
+
3
+ module MongoMapper
4
+ module EmbeddedDocument
5
+ class NotImplemented < StandardError; end
6
+
7
+ def self.included(model)
8
+ model.class_eval do
9
+ extend ClassMethods
10
+ include InstanceMethods
11
+
12
+ extend Associations::ClassMethods
13
+ include Associations::InstanceMethods
14
+
15
+ include Validatable
16
+ include Serialization
17
+ include RailsCompatibility
18
+ end
19
+ end
20
+
21
+ module ClassMethods
22
+
23
+ def keys
24
+ @keys ||= if parent = parent_model
25
+ parent.keys.dup
26
+ else
27
+ HashWithIndifferentAccess.new
28
+ end
29
+ end
30
+
31
+ def key(name, type, options={})
32
+ key = Key.new(name, type, options)
33
+ keys[key.name] = key
34
+ apply_validations_for(key)
35
+ create_indexes_for(key)
36
+ key
37
+ end
38
+
39
+ def ensure_index(name_or_array, options={})
40
+ keys_to_index = if name_or_array.is_a?(Array)
41
+ name_or_array.map { |pair| [pair[0], pair[1]] }
42
+ else
43
+ name_or_array
44
+ end
45
+
46
+ collection.create_index(keys_to_index, options.delete(:unique))
47
+ end
48
+
49
+ def embeddable?
50
+ !self.ancestors.include?(Document)
51
+ end
52
+
53
+ def parent_model
54
+ if parent = ancestors[1]
55
+ parent if parent.ancestors.include?(EmbeddedDocument)
56
+ end
57
+ end
58
+
59
+ private
60
+ def create_indexes_for(key)
61
+ ensure_index key.name if key.options[:index]
62
+ end
63
+
64
+ def apply_validations_for(key)
65
+ attribute = key.name.to_sym
66
+
67
+ if key.options[:required]
68
+ validates_presence_of(attribute, :message => "#{attribute}.not.present")
69
+ end
70
+
71
+ if key.options[:valid]
72
+ validates_true_for attribute, :logic => lambda{ ( valid = send( attribute ).try( :valid? ) ).nil? ? true : valid }, :message => "#{attribute}.record.invalid"
73
+ end
74
+
75
+ if key.options[:unique]
76
+ validates_uniqueness_of(attribute, :message => "#{attribute}.already.taken")
77
+ end
78
+
79
+ if key.options[:numeric]
80
+ number_options = key.type == Integer ? { :only_integer => true, :message => "#{attribute}.not.integer" } : { :message => "#{attribute}.not.numeric" }
81
+ validates_numericality_of(attribute, number_options)
82
+ end
83
+
84
+ if key.options[:format]
85
+ validates_format_of(attribute, :with => key.options[:format], :message => "#{attribute}.format.invalid")
86
+ end
87
+
88
+ if key.options[:length]
89
+ length_options = case key.options[:length]
90
+ when Integer
91
+ {:minimum => 0, :maximum => key.options[:length], :message => "#{attribute}.exceeds.max_length"}
92
+ when Range
93
+ {:within => key.options[:length], :message => "#{attribute}.length.out_of_bounds"}
94
+ when Hash
95
+ key.options[:length].merge!(:message => "#{attribute}.length.invalid")
96
+ end
97
+ validates_length_of(attribute, length_options)
98
+ end
99
+ end
100
+
101
+ def validates_uniqueness_of(*args)
102
+ add_validations(args, MongoMapper::Validations::ValidatesUniquenessOf)
103
+ end
104
+
105
+ def validates_exclusion_of(*args)
106
+ add_validations(args, MongoMapper::Validations::ValidatesExclusionOf)
107
+ end
108
+
109
+ def validates_inclusion_of(*args)
110
+ add_validations(args, MongoMapper::Validations::ValidatesInclusionOf)
111
+ end
112
+
113
+ end
114
+
115
+ module InstanceMethods
116
+
117
+ #
118
+ # Metadata attributes for fancy magic
119
+ # e.g. full_key_name e.g. account.login, name.first
120
+ # user.account.new? => user.new?
121
+ # validates_uniqueness_of in embedded document
122
+ # callbacks in embedded document (TODO)
123
+ #
124
+ attr_reader :_root # The Mongo::Document which is the root doc
125
+ attr_reader :_parent # The parent document. For root parent is nil
126
+ attr_reader :_parent_key # The key name of the embedded document in parent doc
127
+
128
+ def initialize(attrs={})
129
+ @_root = self.class.include?(MongoMapper::Document) ? self : nil
130
+ @_parent = nil
131
+ @_parent_key = nil
132
+ unless attrs.nil?
133
+ initialize_associations(attrs)
134
+ self.attributes = attrs
135
+ end
136
+ end
137
+
138
+ def new?
139
+ _root.try(:new?)
140
+ end
141
+
142
+ def full_key_path(name)
143
+ _parent ? _parent.full_key_path("#{_parent_key}.#{name}") : name
144
+ end
145
+
146
+ def attributes=(attrs)
147
+ return if attrs.blank?
148
+ attrs.each_pair do |key_name, value|
149
+ if writer?(key_name)
150
+ write_attribute(key_name, value)
151
+ else
152
+ writer_method ="#{key_name}="
153
+ self.send(writer_method, value) if respond_to?(writer_method)
154
+ end
155
+ end
156
+ end
157
+
158
+ def attributes
159
+ self.class.keys.inject(HashWithIndifferentAccess.new) do |attributes, key_hash|
160
+ name, key = key_hash
161
+ value = value_for_key(key)
162
+ attributes[name] = value unless value.nil?
163
+ attributes
164
+ end
165
+ end
166
+
167
+ def reader?(name)
168
+ defined_key_names.include?(name.to_s)
169
+ end
170
+
171
+ def writer?(name)
172
+ name = name.to_s
173
+ name = name.chop if name.ends_with?('=')
174
+ reader?(name)
175
+ end
176
+
177
+ def before_typecast_reader?(name)
178
+ name.to_s.match(/^(.*)_before_typecast$/) && reader?($1)
179
+ end
180
+
181
+ def [](name)
182
+ read_attribute(name)
183
+ end
184
+
185
+ def []=(name, value)
186
+ write_attribute(name, value)
187
+ end
188
+
189
+ def method_missing(method, *args, &block)
190
+ attribute = method.to_s
191
+
192
+ if reader?(attribute)
193
+ read_attribute(attribute)
194
+ elsif writer?(attribute)
195
+ write_attribute(attribute.chop, args[0])
196
+ elsif before_typecast_reader?(attribute)
197
+ read_attribute_before_typecast(attribute.gsub(/_before_typecast$/, ''))
198
+ else
199
+ super
200
+ end
201
+ end
202
+
203
+ def ==(other)
204
+ other.is_a?(self.class) && attributes == other.attributes
205
+ end
206
+
207
+ def inspect
208
+ attributes_as_nice_string = defined_key_names.collect do |name|
209
+ "#{name}: #{read_attribute(name)}"
210
+ end.join(", ")
211
+ "#<#{self.class} #{attributes_as_nice_string}>"
212
+ end
213
+
214
+ alias :respond_to_without_attributes? :respond_to?
215
+
216
+ def respond_to?(method, include_private=false)
217
+ return true if reader?(method) || writer?(method) || before_typecast_reader?(method)
218
+ super
219
+ end
220
+
221
+ private
222
+ def value_for_key(key)
223
+ if key.native?
224
+ read_attribute(key.name)
225
+ else
226
+ embedded_document = read_attribute(key.name)
227
+ embedded_document && embedded_document.attributes
228
+ end
229
+ end
230
+
231
+ def read_attribute(name)
232
+ defined_key(name).get(instance_variable_get("@#{name}"))
233
+ end
234
+
235
+ def read_attribute_before_typecast(name)
236
+ instance_variable_get("@#{name}_before_typecast")
237
+ end
238
+
239
+ def write_attribute(name, value)
240
+ instance_variable_set "@#{name}_before_typecast", value
241
+ key = defined_key(name)
242
+ result = instance_variable_set "@#{name}", key.set(value)
243
+ if key.type.include?(MongoMapper::EmbeddedDocument) && !key.type.include?(MongoMapper::Document)
244
+ attribute_val = read_attribute(name)
245
+ attribute_val.instance_variable_set("@_root", self._root)
246
+ attribute_val.instance_variable_set("@_parent", self)
247
+ attribute_val.instance_variable_set("@_parent_key", name)
248
+ end
249
+ end
250
+
251
+ def defined_key(name)
252
+ self.class.keys[name]
253
+ end
254
+
255
+ def defined_key_names
256
+ self.class.keys.keys
257
+ end
258
+
259
+ def only_defined_keys(hash={})
260
+ defined_key_names = defined_key_names()
261
+ hash.delete_if { |k, v| !defined_key_names.include?(k.to_s) }
262
+ end
263
+
264
+ def embedded_association_attributes
265
+ embedded_attributes = HashWithIndifferentAccess.new
266
+ self.class.associations.each_pair do |name, association|
267
+
268
+ if association.type == :many && association.klass.embeddable?
269
+ if documents = instance_variable_get(association.ivar)
270
+ embedded_attributes[name] = documents.collect do |item|
271
+ attributes_hash = item.attributes
272
+
273
+ item.send(:embedded_association_attributes).each_pair do |association_name, association_value|
274
+ attributes_hash[association_name] = association_value
275
+ end
276
+
277
+ attributes_hash
278
+ end
279
+ end
280
+ end
281
+ end
282
+
283
+ embedded_attributes
284
+ end
285
+
286
+ def initialize_associations(attrs={})
287
+ self.class.associations.each_pair do |name, association|
288
+ if collection = attrs.delete(name)
289
+ __send__("#{association.name}=", collection)
290
+ end
291
+ end
292
+ end
293
+ end
294
+ end
295
+ end