crnixon-mongomapper 0.2.0

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 (41) hide show
  1. data/.gitignore +6 -0
  2. data/History +24 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +37 -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 +262 -0
  18. data/lib/mongomapper/embedded_document.rb +226 -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/rails_compatibility.rb +23 -0
  23. data/lib/mongomapper/save_with_validation.rb +19 -0
  24. data/lib/mongomapper/serialization.rb +55 -0
  25. data/lib/mongomapper/serializers/json_serializer.rb +77 -0
  26. data/lib/mongomapper/validations.rb +47 -0
  27. data/mongomapper.gemspec +104 -0
  28. data/test/serializers/test_json_serializer.rb +104 -0
  29. data/test/test_associations.rb +174 -0
  30. data/test/test_callbacks.rb +84 -0
  31. data/test/test_document.rb +944 -0
  32. data/test/test_embedded_document.rb +253 -0
  33. data/test/test_finder_options.rb +148 -0
  34. data/test/test_helper.rb +62 -0
  35. data/test/test_key.rb +200 -0
  36. data/test/test_mongomapper.rb +28 -0
  37. data/test/test_observing.rb +101 -0
  38. data/test/test_rails_compatibility.rb +29 -0
  39. data/test/test_serializations.rb +54 -0
  40. data/test/test_validations.rb +393 -0
  41. metadata +154 -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,262 @@
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
+ include RailsCompatibility
13
+ extend ClassMethods
14
+
15
+ key :_id, String
16
+ key :created_at, Time
17
+ key :updated_at, Time
18
+ end
19
+
20
+ descendants << model
21
+ end
22
+
23
+ def self.descendants
24
+ @descendants ||= Set.new
25
+ end
26
+
27
+ module ClassMethods
28
+ def find(*args)
29
+ options = args.extract_options!
30
+
31
+ case args.first
32
+ when :first then find_first(options)
33
+ when :last then find_last(options)
34
+ when :all then find_every(options)
35
+ else find_from_ids(args)
36
+ end
37
+ end
38
+
39
+ def first(options={})
40
+ find_first(options)
41
+ end
42
+
43
+ def last(options={})
44
+ find_last(options)
45
+ end
46
+
47
+ def all(options={})
48
+ find_every(options)
49
+ end
50
+
51
+ def find_by_id(id)
52
+ if doc = collection.find_first({:_id => id})
53
+ new(doc)
54
+ end
55
+ end
56
+
57
+ # TODO: remove the rescuing when ruby driver works correctly
58
+ def count(conditions={})
59
+ collection.count(FinderOptions.to_mongo_criteria(conditions))
60
+ end
61
+
62
+ def create(*docs)
63
+ instances = []
64
+ docs.flatten.each do |attrs|
65
+ doc = new(attrs); doc.save
66
+ instances << doc
67
+ end
68
+ instances.size == 1 ? instances[0] : instances
69
+ end
70
+
71
+ # For updating single document
72
+ # Person.update(1, {:foo => 'bar'})
73
+ #
74
+ # For updating multiple documents at once:
75
+ # Person.update({'1' => {:foo => 'bar'}, '2' => {:baz => 'wick'}})
76
+ def update(*args)
77
+ updating_multiple = args.length == 1
78
+ if updating_multiple
79
+ update_multiple(args[0])
80
+ else
81
+ id, attributes = args
82
+ update_single(id, attributes)
83
+ end
84
+ end
85
+
86
+ def delete(*ids)
87
+ collection.remove(:_id => {'$in' => ids.flatten})
88
+ end
89
+
90
+ def delete_all(conditions={})
91
+ collection.remove(FinderOptions.to_mongo_criteria(conditions))
92
+ end
93
+
94
+ def destroy(*ids)
95
+ find_some(ids.flatten).each(&:destroy)
96
+ end
97
+
98
+ def destroy_all(conditions={})
99
+ find(:all, :conditions => conditions).each(&:destroy)
100
+ end
101
+
102
+ def connection(mongo_connection=nil)
103
+ if mongo_connection.nil?
104
+ @connection ||= MongoMapper.connection
105
+ else
106
+ @connection = mongo_connection
107
+ end
108
+ @connection
109
+ end
110
+
111
+ def database(name=nil)
112
+ if name.nil?
113
+ @database ||= MongoMapper.database
114
+ else
115
+ @database = connection.db(name)
116
+ end
117
+ @database
118
+ end
119
+
120
+ def collection(name=nil)
121
+ if name.nil?
122
+ @collection ||= database.collection(self.to_s.demodulize.tableize)
123
+ else
124
+ @collection = database.collection(name)
125
+ end
126
+ @collection
127
+ end
128
+
129
+ def validates_uniqueness_of(*args)
130
+ add_validations(args, MongoMapper::Validations::ValidatesUniquenessOf)
131
+ end
132
+
133
+ def validates_exclusion_of(*args)
134
+ add_validations(args, MongoMapper::Validations::ValidatesExclusionOf)
135
+ end
136
+
137
+ def validates_inclusion_of(*args)
138
+ add_validations(args, MongoMapper::Validations::ValidatesInclusionOf)
139
+ end
140
+
141
+ private
142
+ def find_every(options)
143
+ criteria, options = FinderOptions.new(options).to_a
144
+ collection.find(criteria, options).to_a.map { |doc| new(doc) }
145
+ end
146
+
147
+ def find_first(options)
148
+ find_every(options.merge(:limit => 1, :order => 'created_at')).first
149
+ end
150
+
151
+ def find_last(options)
152
+ find_every(options.merge(:limit => 1, :order => 'created_at desc')).first
153
+ end
154
+
155
+ def find_some(ids)
156
+ documents = find_every(:conditions => {'_id' => ids})
157
+ if ids.size == documents.size
158
+ documents
159
+ else
160
+ raise DocumentNotFound, "Couldn't find all of the ids (#{ids.to_sentence}). Found #{documents.size}, but was expecting #{ids.size}"
161
+ end
162
+ end
163
+
164
+ def find_from_ids(*ids)
165
+ ids = ids.flatten.compact.uniq
166
+
167
+ case ids.size
168
+ when 0
169
+ raise(DocumentNotFound, "Couldn't find without an ID")
170
+ when 1
171
+ find_by_id(ids[0]) || raise(DocumentNotFound, "Document with id of #{ids[0]} does not exist in collection named #{collection.name}")
172
+ else
173
+ find_some(ids)
174
+ end
175
+ end
176
+
177
+ def update_single(id, attrs)
178
+ if id.blank? || attrs.blank? || !attrs.is_a?(Hash)
179
+ raise ArgumentError, "Updating a single document requires an id and a hash of attributes"
180
+ end
181
+
182
+ find(id).update_attributes(attrs)
183
+ end
184
+
185
+ def update_multiple(docs)
186
+ unless docs.is_a?(Hash)
187
+ raise ArgumentError, "Updating multiple documents takes 1 argument and it must be hash"
188
+ end
189
+ instances = []
190
+ docs.each_pair { |id, attrs| instances << update(id, attrs) }
191
+ instances
192
+ end
193
+ end
194
+
195
+ module InstanceMethods
196
+ def collection
197
+ self.class.collection
198
+ end
199
+
200
+ def new?
201
+ read_attribute('_id').blank? || self.class.find_by_id(id).blank?
202
+ end
203
+
204
+ def save
205
+ create_or_update
206
+ end
207
+
208
+ def save!
209
+ create_or_update || raise(DocumentNotValid.new(self))
210
+ end
211
+
212
+ def update_attributes(attrs={})
213
+ self.attributes = attrs
214
+ save
215
+ self
216
+ end
217
+
218
+ def destroy
219
+ collection.remove(:_id => id) unless new?
220
+ freeze
221
+ end
222
+
223
+ def ==(other)
224
+ other.is_a?(self.class) && id == other.id
225
+ end
226
+
227
+ def id
228
+ read_attribute('_id')
229
+ end
230
+
231
+ private
232
+ def create_or_update
233
+ result = new? ? create : update
234
+ result != false
235
+ end
236
+
237
+ def create
238
+ write_attribute('_id', generate_id) if read_attribute('_id').blank?
239
+ update_timestamps
240
+ save_to_collection
241
+ end
242
+
243
+ def update
244
+ update_timestamps
245
+ save_to_collection
246
+ end
247
+
248
+ def save_to_collection
249
+ collection.save(attributes.merge!(embedded_association_attributes))
250
+ end
251
+
252
+ def update_timestamps
253
+ write_attribute('created_at', Time.now.utc) if new?
254
+ write_attribute('updated_at', Time.now.utc)
255
+ end
256
+
257
+ def generate_id
258
+ XGen::Mongo::Driver::ObjectID.new
259
+ end
260
+ end
261
+ end # Document
262
+ end # MongoMapper
@@ -0,0 +1,226 @@
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
+ end
18
+ end
19
+
20
+ module ClassMethods
21
+ def keys
22
+ @keys ||= HashWithIndifferentAccess.new
23
+ end
24
+
25
+ def key(name, type, options={})
26
+ key = Key.new(name, type, options)
27
+ keys[key.name] = key
28
+ apply_validations_for(key)
29
+ create_indexes_for(key)
30
+ key
31
+ end
32
+
33
+ def ensure_index(name_or_array, options={})
34
+ keys_to_index = if name_or_array.is_a?(Array)
35
+ name_or_array.map { |pair| [pair[0], pair[1]] }
36
+ else
37
+ name_or_array
38
+ end
39
+
40
+ collection.create_index(keys_to_index, options.delete(:unique))
41
+ end
42
+
43
+ def embeddable?
44
+ !self.ancestors.include?(Document)
45
+ end
46
+
47
+ private
48
+ def create_indexes_for(key)
49
+ ensure_index key.name if key.options[:index]
50
+ end
51
+
52
+ def apply_validations_for(key)
53
+ attribute = key.name.to_sym
54
+
55
+ if key.options[:required]
56
+ validates_presence_of(attribute)
57
+ end
58
+
59
+ if key.options[:unique]
60
+ validates_uniqueness_of(attribute)
61
+ end
62
+
63
+ if key.options[:numeric]
64
+ number_options = key.type == Integer ? {:only_integer => true} : {}
65
+ validates_numericality_of(attribute, number_options)
66
+ end
67
+
68
+ if key.options[:format]
69
+ validates_format_of(attribute, :with => key.options[:format])
70
+ end
71
+
72
+ if key.options[:length]
73
+ length_options = case key.options[:length]
74
+ when Integer
75
+ {:minimum => 0, :maximum => key.options[:length]}
76
+ when Range
77
+ {:within => key.options[:length]}
78
+ when Hash
79
+ key.options[:length]
80
+ end
81
+ validates_length_of(attribute, length_options)
82
+ end
83
+ end
84
+
85
+ end
86
+
87
+ module InstanceMethods
88
+ def initialize(attrs={})
89
+ unless attrs.nil?
90
+ initialize_associations(attrs)
91
+ self.attributes = attrs
92
+ end
93
+ end
94
+
95
+ def attributes=(attrs)
96
+ return if attrs.blank?
97
+ attrs.each_pair do |key_name, value|
98
+ if writer?(key_name)
99
+ write_attribute(key_name, value)
100
+ else
101
+ writer_method ="#{key_name}="
102
+ self.send(writer_method, value) if respond_to?(writer_method)
103
+ end
104
+ end
105
+ end
106
+
107
+ def attributes
108
+ self.class.keys.inject(HashWithIndifferentAccess.new) do |attributes, key_hash|
109
+ name, key = key_hash
110
+ value = value_for_key(key)
111
+ attributes[name] = value unless value.nil?
112
+ attributes
113
+ end
114
+ end
115
+
116
+ def reader?(name)
117
+ defined_key_names.include?(name.to_s)
118
+ end
119
+
120
+ def writer?(name)
121
+ name = name.to_s
122
+ name = name.chop if name.ends_with?('=')
123
+ reader?(name)
124
+ end
125
+
126
+ def before_typecast_reader?(name)
127
+ name.to_s.match(/^(.*)_before_typecast$/) && reader?($1)
128
+ end
129
+
130
+ def [](name)
131
+ read_attribute(name)
132
+ end
133
+
134
+ def []=(name, value)
135
+ write_attribute(name, value)
136
+ end
137
+
138
+ def method_missing(method, *args, &block)
139
+ attribute = method.to_s
140
+
141
+ if reader?(attribute)
142
+ read_attribute(attribute)
143
+ elsif writer?(attribute)
144
+ write_attribute(attribute.chop, args[0])
145
+ elsif before_typecast_reader?(attribute)
146
+ read_attribute_before_typecast(attribute.gsub(/_before_typecast$/, ''))
147
+ else
148
+ super
149
+ end
150
+ end
151
+
152
+ def ==(other)
153
+ other.is_a?(self.class) && attributes == other.attributes
154
+ end
155
+
156
+ def inspect
157
+ attributes_as_nice_string = defined_key_names.collect do |name|
158
+ "#{name}: #{read_attribute(name)}"
159
+ end.join(", ")
160
+ "#<#{self.class} #{attributes_as_nice_string}>"
161
+ end
162
+
163
+ alias :respond_to_without_attributes? :respond_to?
164
+
165
+ def respond_to?(method, include_private=false)
166
+ return true if reader?(method) || writer?(method) || before_typecast_reader?(method)
167
+ super
168
+ end
169
+
170
+ private
171
+ def value_for_key(key)
172
+ if key.native?
173
+ read_attribute(key.name)
174
+ else
175
+ embedded_document = read_attribute(key.name)
176
+ embedded_document && embedded_document.attributes
177
+ end
178
+ end
179
+
180
+ def read_attribute(name)
181
+ defined_key(name).get(instance_variable_get("@#{name}"))
182
+ end
183
+
184
+ def read_attribute_before_typecast(name)
185
+ instance_variable_get("@#{name}_before_typecast")
186
+ end
187
+
188
+ def write_attribute(name, value)
189
+ instance_variable_set "@#{name}_before_typecast", value
190
+ instance_variable_set "@#{name}", defined_key(name).set(value)
191
+ end
192
+
193
+ def defined_key(name)
194
+ self.class.keys[name]
195
+ end
196
+
197
+ def defined_key_names
198
+ self.class.keys.keys
199
+ end
200
+
201
+ def only_defined_keys(hash={})
202
+ defined_key_names = defined_key_names()
203
+ hash.delete_if { |k, v| !defined_key_names.include?(k.to_s) }
204
+ end
205
+
206
+ def embedded_association_attributes
207
+ attributes = HashWithIndifferentAccess.new
208
+ self.class.associations.each_pair do |name, association|
209
+ if association.type == :many && association.klass.embeddable?
210
+ vals = instance_variable_get(association.ivar)
211
+ attributes[name] = vals.collect { |item| item.attributes } if vals
212
+ end
213
+ end
214
+ attributes
215
+ end
216
+
217
+ def initialize_associations(attrs={})
218
+ self.class.associations.each_pair do |name, association|
219
+ if collection = attrs.delete(name)
220
+ __send__("#{association.name}=", collection)
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end