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