crnixon-mongomapper 0.2.0

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