djsun-mongomapper 0.3.5.5 → 0.4.1.2
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/README.rdoc +38 -38
- data/Rakefile +87 -73
- data/VERSION +1 -1
- data/lib/mongomapper.rb +67 -71
- data/lib/mongomapper/associations.rb +86 -84
- data/lib/mongomapper/associations/belongs_to_polymorphic_proxy.rb +34 -34
- data/lib/mongomapper/associations/many_embedded_proxy.rb +67 -17
- data/lib/mongomapper/associations/proxy.rb +74 -73
- data/lib/mongomapper/document.rb +342 -348
- data/lib/mongomapper/embedded_document.rb +354 -274
- data/lib/mongomapper/finder_options.rb +84 -84
- data/lib/mongomapper/key.rb +32 -76
- data/lib/mongomapper/rails_compatibility/document.rb +14 -14
- data/lib/mongomapper/rails_compatibility/embedded_document.rb +26 -24
- data/lib/mongomapper/support.rb +156 -29
- data/lib/mongomapper/validations.rb +69 -47
- data/test/custom_matchers.rb +48 -0
- data/test/functional/associations/test_belongs_to_polymorphic_proxy.rb +53 -56
- data/test/functional/associations/test_belongs_to_proxy.rb +48 -49
- data/test/functional/associations/test_many_documents_as_proxy.rb +208 -253
- data/test/functional/associations/test_many_embedded_polymorphic_proxy.rb +130 -130
- data/test/functional/associations/test_many_embedded_proxy.rb +168 -106
- data/test/functional/associations/test_many_polymorphic_proxy.rb +261 -262
- data/test/functional/test_binary.rb +21 -0
- data/test/functional/test_document.rb +946 -952
- data/test/functional/test_embedded_document.rb +98 -0
- data/test/functional/test_pagination.rb +87 -80
- data/test/functional/test_rails_compatibility.rb +29 -29
- data/test/functional/test_validations.rb +262 -172
- data/test/models.rb +169 -169
- data/test/test_helper.rb +28 -66
- data/test/unit/serializers/test_json_serializer.rb +193 -193
- data/test/unit/test_document.rb +161 -123
- data/test/unit/test_embedded_document.rb +643 -547
- data/test/unit/test_finder_options.rb +183 -183
- data/test/unit/test_key.rb +175 -247
- data/test/unit/test_rails_compatibility.rb +38 -33
- data/test/unit/test_serializations.rb +52 -52
- data/test/unit/test_support.rb +268 -0
- data/test/unit/test_time_zones.rb +40 -0
- data/test/unit/test_validations.rb +499 -258
- metadata +22 -12
- data/History +0 -76
- data/mongomapper.gemspec +0 -145
@@ -1,34 +1,34 @@
|
|
1
|
-
module MongoMapper
|
2
|
-
module Associations
|
3
|
-
class BelongsToPolymorphicProxy < Proxy
|
4
|
-
def replace(doc)
|
5
|
-
if doc
|
6
|
-
doc.save if doc.new?
|
7
|
-
id, type = doc.id, doc.class.name
|
8
|
-
end
|
9
|
-
|
10
|
-
@owner.send("#{@association.foreign_key}=", id)
|
11
|
-
@owner.send("#{@association.type_key_name}=", type)
|
12
|
-
reset
|
13
|
-
end
|
14
|
-
|
15
|
-
protected
|
16
|
-
def find_target
|
17
|
-
if proxy_id && proxy_class
|
18
|
-
proxy_class.find_by_id(proxy_id)
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
def proxy_id
|
23
|
-
@proxy_id ||= @owner.send(@association.foreign_key)
|
24
|
-
end
|
25
|
-
|
26
|
-
def proxy_class
|
27
|
-
@proxy_class ||= begin
|
28
|
-
klass = @owner.send(@association.type_key_name)
|
29
|
-
klass && klass.constantize
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
1
|
+
module MongoMapper
|
2
|
+
module Associations
|
3
|
+
class BelongsToPolymorphicProxy < Proxy
|
4
|
+
def replace(doc)
|
5
|
+
if doc
|
6
|
+
doc.save if doc.new?
|
7
|
+
id, type = doc.id, doc.class.name
|
8
|
+
end
|
9
|
+
|
10
|
+
@owner.send("#{@association.foreign_key}=", id)
|
11
|
+
@owner.send("#{@association.type_key_name}=", type)
|
12
|
+
reset
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
def find_target
|
17
|
+
if proxy_id && proxy_class
|
18
|
+
proxy_class.find_by_id(proxy_id)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def proxy_id
|
23
|
+
@proxy_id ||= @owner.send(@association.foreign_key)
|
24
|
+
end
|
25
|
+
|
26
|
+
def proxy_class
|
27
|
+
@proxy_class ||= begin
|
28
|
+
klass = @owner.send(@association.type_key_name)
|
29
|
+
klass && klass.constantize
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -1,17 +1,67 @@
|
|
1
|
-
module MongoMapper
|
2
|
-
module Associations
|
3
|
-
class ManyEmbeddedProxy < Proxy
|
4
|
-
def replace(v)
|
5
|
-
@_values = v.map { |e| e.kind_of?(EmbeddedDocument) ? e.attributes : e }
|
6
|
-
reset
|
7
|
-
end
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
1
|
+
module MongoMapper
|
2
|
+
module Associations
|
3
|
+
class ManyEmbeddedProxy < Proxy
|
4
|
+
def replace(v)
|
5
|
+
@_values = v.map { |e| e.kind_of?(EmbeddedDocument) ? e.attributes : e }
|
6
|
+
reset
|
7
|
+
end
|
8
|
+
|
9
|
+
def build(opts={})
|
10
|
+
owner = @owner
|
11
|
+
child = @association.klass.new(opts)
|
12
|
+
assign_parent_reference(child)
|
13
|
+
child._root_document = owner
|
14
|
+
self << child
|
15
|
+
child
|
16
|
+
end
|
17
|
+
|
18
|
+
def find(opts)
|
19
|
+
case opts
|
20
|
+
when :all
|
21
|
+
self
|
22
|
+
when String
|
23
|
+
if load_target
|
24
|
+
child = @target.detect {|item| item.id == opts}
|
25
|
+
assign_parent_reference(child)
|
26
|
+
child
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def <<(*docs)
|
32
|
+
if load_target
|
33
|
+
root = @owner._root_document || @owner
|
34
|
+
docs.each do |doc|
|
35
|
+
doc._root_document = root
|
36
|
+
@target << doc
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
alias_method :push, :<<
|
41
|
+
alias_method :concat, :<<
|
42
|
+
|
43
|
+
protected
|
44
|
+
def find_target
|
45
|
+
(@_values || []).map do |e|
|
46
|
+
child = @association.klass.new(e)
|
47
|
+
assign_parent_reference(child)
|
48
|
+
child
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def assign_parent_reference(child)
|
55
|
+
return unless child && @owner
|
56
|
+
return if @owner.class.name.blank?
|
57
|
+
owner = @owner
|
58
|
+
child.class_eval do
|
59
|
+
define_method(owner.class.name.underscore) do
|
60
|
+
owner
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -1,73 +1,74 @@
|
|
1
|
-
module MongoMapper
|
2
|
-
module Associations
|
3
|
-
class Proxy < BasicObject
|
4
|
-
attr_reader :owner, :association
|
5
|
-
|
6
|
-
def initialize(owner, association)
|
7
|
-
@owner = owner
|
8
|
-
@association = association
|
9
|
-
reset
|
10
|
-
end
|
11
|
-
|
12
|
-
def respond_to?(*methods)
|
13
|
-
(load_target && @target.respond_to?(*methods))
|
14
|
-
end
|
15
|
-
|
16
|
-
def reset
|
17
|
-
@target = nil
|
18
|
-
end
|
19
|
-
|
20
|
-
def reload_target
|
21
|
-
reset
|
22
|
-
load_target
|
23
|
-
self
|
24
|
-
end
|
25
|
-
|
26
|
-
def send(method, *args)
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
#
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
end
|
1
|
+
module MongoMapper
|
2
|
+
module Associations
|
3
|
+
class Proxy < BasicObject
|
4
|
+
attr_reader :owner, :association
|
5
|
+
|
6
|
+
def initialize(owner, association)
|
7
|
+
@owner = owner
|
8
|
+
@association = association
|
9
|
+
reset
|
10
|
+
end
|
11
|
+
|
12
|
+
def respond_to?(*methods)
|
13
|
+
(load_target && @target.respond_to?(*methods))
|
14
|
+
end
|
15
|
+
|
16
|
+
def reset
|
17
|
+
@target = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def reload_target
|
21
|
+
reset
|
22
|
+
load_target
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def send(method, *args)
|
27
|
+
return super if methods.include? method.to_s
|
28
|
+
load_target
|
29
|
+
@target.send(method, *args)
|
30
|
+
end
|
31
|
+
|
32
|
+
def replace(v)
|
33
|
+
raise NotImplementedError
|
34
|
+
end
|
35
|
+
|
36
|
+
def inspect
|
37
|
+
load_target
|
38
|
+
@target.inspect
|
39
|
+
end
|
40
|
+
|
41
|
+
def nil?
|
42
|
+
load_target
|
43
|
+
@target.nil?
|
44
|
+
end
|
45
|
+
|
46
|
+
protected
|
47
|
+
def method_missing(method, *args)
|
48
|
+
if load_target
|
49
|
+
if block_given?
|
50
|
+
@target.send(method, *args) { |*block_args| yield(*block_args) }
|
51
|
+
else
|
52
|
+
@target.send(method, *args)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def load_target
|
58
|
+
@target ||= find_target
|
59
|
+
end
|
60
|
+
|
61
|
+
def find_target
|
62
|
+
raise NotImplementedError
|
63
|
+
end
|
64
|
+
|
65
|
+
# Array#flatten has problems with recursive arrays. Going one level
|
66
|
+
# deeper solves the majority of the problems.
|
67
|
+
def flatten_deeper(array)
|
68
|
+
array.collect do |element|
|
69
|
+
(element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element
|
70
|
+
end.flatten
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/mongomapper/document.rb
CHANGED
@@ -1,348 +1,342 @@
|
|
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::Document
|
13
|
-
extend
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
options
|
42
|
-
|
43
|
-
|
44
|
-
collection.
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
end
|
100
|
-
|
101
|
-
def
|
102
|
-
criteria = FinderOptions.to_mongo_criteria(
|
103
|
-
collection.remove(criteria)
|
104
|
-
end
|
105
|
-
|
106
|
-
def
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
end
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
end
|
159
|
-
|
160
|
-
protected
|
161
|
-
def method_missing(method, *args)
|
162
|
-
finder = DynamicFinder.new(self, method)
|
163
|
-
|
164
|
-
if finder.valid?
|
165
|
-
meta_def(finder.options[:method]) do |*args|
|
166
|
-
find_with_args(args, finder.options)
|
167
|
-
end
|
168
|
-
|
169
|
-
send(finder.options[:method], *args)
|
170
|
-
else
|
171
|
-
super
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
|
-
private
|
176
|
-
def find_every(options)
|
177
|
-
criteria, options = FinderOptions.new(options).to_a
|
178
|
-
collection.find(criteria, options).to_a.map { |doc| new(doc) }
|
179
|
-
end
|
180
|
-
|
181
|
-
def find_first(options)
|
182
|
-
options.merge!(:limit => 1)
|
183
|
-
find_every({:order => '$natural asc'}.merge(options))[0]
|
184
|
-
end
|
185
|
-
|
186
|
-
def find_last(options)
|
187
|
-
options.merge!(:limit => 1)
|
188
|
-
options[:order] = invert_order_clause(options)
|
189
|
-
find_every(options)[0]
|
190
|
-
#find_every({:order => '$natural desc'}.merge(invert_order_clause(options)))[0]
|
191
|
-
end
|
192
|
-
|
193
|
-
def invert_order_clause(options)
|
194
|
-
return '$natural desc' unless options[:order]
|
195
|
-
options[:order].split(',').map do |order_segment|
|
196
|
-
if order_segment =~ /\sasc/i
|
197
|
-
order_segment.sub /\sasc/i, ' desc'
|
198
|
-
elsif order_segment =~ /\sdesc/i
|
199
|
-
order_segment.sub /\sdesc/i, ' asc'
|
200
|
-
else
|
201
|
-
"#{order_segment.strip} desc"
|
202
|
-
end
|
203
|
-
end.join(',')
|
204
|
-
end
|
205
|
-
|
206
|
-
def find_some(ids, options={})
|
207
|
-
documents = find_every(options.deep_merge(:conditions => {'_id' => ids}))
|
208
|
-
if ids.size == documents.size
|
209
|
-
documents
|
210
|
-
else
|
211
|
-
raise DocumentNotFound, "Couldn't find all of the ids (#{ids.to_sentence}). Found #{documents.size}, but was expecting #{ids.size}"
|
212
|
-
end
|
213
|
-
end
|
214
|
-
|
215
|
-
def find_one(id, options={})
|
216
|
-
if doc = find_every(options.deep_merge(:conditions => {:_id => id})).first
|
217
|
-
doc
|
218
|
-
else
|
219
|
-
raise DocumentNotFound, "Document with id of #{id} does not exist in collection named #{collection.name}"
|
220
|
-
end
|
221
|
-
end
|
222
|
-
|
223
|
-
def find_from_ids(ids, options={})
|
224
|
-
ids = ids.flatten.compact.uniq
|
225
|
-
|
226
|
-
case ids.size
|
227
|
-
when 0
|
228
|
-
raise(DocumentNotFound, "Couldn't find without an ID")
|
229
|
-
when 1
|
230
|
-
find_one(ids[0], options)
|
231
|
-
else
|
232
|
-
find_some(ids, options)
|
233
|
-
end
|
234
|
-
end
|
235
|
-
|
236
|
-
def find_with_args(args, options)
|
237
|
-
attributes, = {}
|
238
|
-
find_options = args.extract_options!.deep_merge(:conditions => attributes)
|
239
|
-
|
240
|
-
options[:attribute_names].each_with_index do |attr, index|
|
241
|
-
attributes[attr] = args[index]
|
242
|
-
end
|
243
|
-
|
244
|
-
result = find(options[:finder], find_options)
|
245
|
-
|
246
|
-
if result.nil?
|
247
|
-
if options[:bang]
|
248
|
-
raise DocumentNotFound, "Couldn't find Document with #{attributes.inspect} in collection named #{collection.name}"
|
249
|
-
end
|
250
|
-
|
251
|
-
if options[:instantiator]
|
252
|
-
self.send(options[:instantiator], attributes)
|
253
|
-
end
|
254
|
-
else
|
255
|
-
result
|
256
|
-
end
|
257
|
-
end
|
258
|
-
|
259
|
-
def update_single(id, attrs)
|
260
|
-
if id.blank? || attrs.blank? || !attrs.is_a?(Hash)
|
261
|
-
raise ArgumentError, "Updating a single document requires an id and a hash of attributes"
|
262
|
-
end
|
263
|
-
|
264
|
-
doc = find(id)
|
265
|
-
doc.update_attributes(attrs)
|
266
|
-
doc
|
267
|
-
end
|
268
|
-
|
269
|
-
def update_multiple(docs)
|
270
|
-
unless docs.is_a?(Hash)
|
271
|
-
raise ArgumentError, "Updating multiple documents takes 1 argument and it must be hash"
|
272
|
-
end
|
273
|
-
|
274
|
-
instances = []
|
275
|
-
docs.each_pair { |id, attrs| instances << update(id, attrs) }
|
276
|
-
instances
|
277
|
-
end
|
278
|
-
end
|
279
|
-
|
280
|
-
module InstanceMethods
|
281
|
-
def collection
|
282
|
-
self.class.collection
|
283
|
-
end
|
284
|
-
|
285
|
-
def new?
|
286
|
-
read_attribute('_id').blank? || using_custom_id?
|
287
|
-
end
|
288
|
-
|
289
|
-
def save
|
290
|
-
create_or_update
|
291
|
-
end
|
292
|
-
|
293
|
-
def save!
|
294
|
-
create_or_update || raise(DocumentNotValid.new(self))
|
295
|
-
end
|
296
|
-
|
297
|
-
def
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
def
|
312
|
-
|
313
|
-
|
314
|
-
end
|
315
|
-
|
316
|
-
def
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
end
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
end
|
336
|
-
|
337
|
-
def
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
def clear_custom_id_flag
|
344
|
-
@using_custom_id = nil
|
345
|
-
end
|
346
|
-
end
|
347
|
-
end # Document
|
348
|
-
end # MongoMapper
|
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::Document
|
13
|
+
extend Validations::Macros
|
14
|
+
extend ClassMethods
|
15
|
+
|
16
|
+
def self.per_page
|
17
|
+
25
|
18
|
+
end unless respond_to?(:per_page)
|
19
|
+
end
|
20
|
+
|
21
|
+
descendants << model
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.descendants
|
25
|
+
@descendants ||= Set.new
|
26
|
+
end
|
27
|
+
|
28
|
+
module ClassMethods
|
29
|
+
def find(*args)
|
30
|
+
options = args.extract_options!
|
31
|
+
|
32
|
+
case args.first
|
33
|
+
when :first then find_first(options)
|
34
|
+
when :last then find_last(options)
|
35
|
+
when :all then find_every(options)
|
36
|
+
else find_from_ids(args, options)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def paginate(options)
|
41
|
+
per_page = options.delete(:per_page) || self.per_page
|
42
|
+
page = options.delete(:page)
|
43
|
+
total_entries = count(options[:conditions] || {})
|
44
|
+
collection = Pagination::PaginationProxy.new(total_entries, page, per_page)
|
45
|
+
|
46
|
+
options[:limit] = collection.limit
|
47
|
+
options[:skip] = collection.skip
|
48
|
+
|
49
|
+
collection.subject = find_every(options)
|
50
|
+
collection
|
51
|
+
end
|
52
|
+
|
53
|
+
def first(options={})
|
54
|
+
find_first(options)
|
55
|
+
end
|
56
|
+
|
57
|
+
def last(options={})
|
58
|
+
find_last(options)
|
59
|
+
end
|
60
|
+
|
61
|
+
def all(options={})
|
62
|
+
find_every(options)
|
63
|
+
end
|
64
|
+
|
65
|
+
def find_by_id(id)
|
66
|
+
criteria = FinderOptions.to_mongo_criteria(:_id => id)
|
67
|
+
if doc = collection.find_one(criteria)
|
68
|
+
new(doc)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def count(conditions={})
|
73
|
+
collection.find(FinderOptions.to_mongo_criteria(conditions)).count
|
74
|
+
end
|
75
|
+
|
76
|
+
def create(*docs)
|
77
|
+
instances = []
|
78
|
+
docs = [{}] if docs.blank?
|
79
|
+
docs.flatten.each do |attrs|
|
80
|
+
doc = new(attrs); doc.save
|
81
|
+
instances << doc
|
82
|
+
end
|
83
|
+
instances.size == 1 ? instances[0] : instances
|
84
|
+
end
|
85
|
+
|
86
|
+
# For updating single document
|
87
|
+
# Person.update(1, {:foo => 'bar'})
|
88
|
+
#
|
89
|
+
# For updating multiple documents at once:
|
90
|
+
# Person.update({'1' => {:foo => 'bar'}, '2' => {:baz => 'wick'}})
|
91
|
+
def update(*args)
|
92
|
+
updating_multiple = args.length == 1
|
93
|
+
if updating_multiple
|
94
|
+
update_multiple(args[0])
|
95
|
+
else
|
96
|
+
id, attributes = args
|
97
|
+
update_single(id, attributes)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def delete(*ids)
|
102
|
+
criteria = FinderOptions.to_mongo_criteria(:_id => ids.flatten)
|
103
|
+
collection.remove(criteria)
|
104
|
+
end
|
105
|
+
|
106
|
+
def delete_all(conditions={})
|
107
|
+
criteria = FinderOptions.to_mongo_criteria(conditions)
|
108
|
+
collection.remove(criteria)
|
109
|
+
end
|
110
|
+
|
111
|
+
def destroy(*ids)
|
112
|
+
find_some(ids.flatten).each(&:destroy)
|
113
|
+
end
|
114
|
+
|
115
|
+
def destroy_all(conditions={})
|
116
|
+
find(:all, :conditions => conditions).each(&:destroy)
|
117
|
+
end
|
118
|
+
|
119
|
+
def connection(mongo_connection=nil)
|
120
|
+
if mongo_connection.nil?
|
121
|
+
@connection ||= MongoMapper.connection
|
122
|
+
else
|
123
|
+
@connection = mongo_connection
|
124
|
+
end
|
125
|
+
@connection
|
126
|
+
end
|
127
|
+
|
128
|
+
def database(name=nil)
|
129
|
+
if name.nil?
|
130
|
+
@database ||= MongoMapper.database
|
131
|
+
else
|
132
|
+
@database = connection.db(name)
|
133
|
+
end
|
134
|
+
@database
|
135
|
+
end
|
136
|
+
|
137
|
+
# Changes the collection name from the default to whatever you want
|
138
|
+
def set_collection_name(name=nil)
|
139
|
+
@collection = nil
|
140
|
+
@collection_name = name
|
141
|
+
end
|
142
|
+
|
143
|
+
# Returns the collection name, if not set, defaults to class name tableized
|
144
|
+
def collection_name
|
145
|
+
@collection_name ||= self.to_s.demodulize.tableize
|
146
|
+
end
|
147
|
+
|
148
|
+
# Returns the mongo ruby driver collection object
|
149
|
+
def collection
|
150
|
+
@collection ||= database.collection(collection_name)
|
151
|
+
end
|
152
|
+
|
153
|
+
def timestamps!
|
154
|
+
key :created_at, Time
|
155
|
+
key :updated_at, Time
|
156
|
+
|
157
|
+
class_eval { before_save :update_timestamps }
|
158
|
+
end
|
159
|
+
|
160
|
+
protected
|
161
|
+
def method_missing(method, *args)
|
162
|
+
finder = DynamicFinder.new(self, method)
|
163
|
+
|
164
|
+
if finder.valid?
|
165
|
+
meta_def(finder.options[:method]) do |*args|
|
166
|
+
find_with_args(args, finder.options)
|
167
|
+
end
|
168
|
+
|
169
|
+
send(finder.options[:method], *args)
|
170
|
+
else
|
171
|
+
super
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
private
|
176
|
+
def find_every(options)
|
177
|
+
criteria, options = FinderOptions.new(options).to_a
|
178
|
+
collection.find(criteria, options).to_a.map { |doc| new(doc) }
|
179
|
+
end
|
180
|
+
|
181
|
+
def find_first(options)
|
182
|
+
options.merge!(:limit => 1)
|
183
|
+
find_every({:order => '$natural asc'}.merge(options))[0]
|
184
|
+
end
|
185
|
+
|
186
|
+
def find_last(options)
|
187
|
+
options.merge!(:limit => 1)
|
188
|
+
options[:order] = invert_order_clause(options)
|
189
|
+
find_every(options)[0]
|
190
|
+
#find_every({:order => '$natural desc'}.merge(invert_order_clause(options)))[0]
|
191
|
+
end
|
192
|
+
|
193
|
+
def invert_order_clause(options)
|
194
|
+
return '$natural desc' unless options[:order]
|
195
|
+
options[:order].split(',').map do |order_segment|
|
196
|
+
if order_segment =~ /\sasc/i
|
197
|
+
order_segment.sub /\sasc/i, ' desc'
|
198
|
+
elsif order_segment =~ /\sdesc/i
|
199
|
+
order_segment.sub /\sdesc/i, ' asc'
|
200
|
+
else
|
201
|
+
"#{order_segment.strip} desc"
|
202
|
+
end
|
203
|
+
end.join(',')
|
204
|
+
end
|
205
|
+
|
206
|
+
def find_some(ids, options={})
|
207
|
+
documents = find_every(options.deep_merge(:conditions => {'_id' => ids}))
|
208
|
+
if ids.size == documents.size
|
209
|
+
documents
|
210
|
+
else
|
211
|
+
raise DocumentNotFound, "Couldn't find all of the ids (#{ids.to_sentence}). Found #{documents.size}, but was expecting #{ids.size}"
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def find_one(id, options={})
|
216
|
+
if doc = find_every(options.deep_merge(:conditions => {:_id => id})).first
|
217
|
+
doc
|
218
|
+
else
|
219
|
+
raise DocumentNotFound, "Document with id of #{id} does not exist in collection named #{collection.name}"
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def find_from_ids(ids, options={})
|
224
|
+
ids = ids.flatten.compact.uniq
|
225
|
+
|
226
|
+
case ids.size
|
227
|
+
when 0
|
228
|
+
raise(DocumentNotFound, "Couldn't find without an ID")
|
229
|
+
when 1
|
230
|
+
find_one(ids[0], options)
|
231
|
+
else
|
232
|
+
find_some(ids, options)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def find_with_args(args, options)
|
237
|
+
attributes, = {}
|
238
|
+
find_options = args.extract_options!.deep_merge(:conditions => attributes)
|
239
|
+
|
240
|
+
options[:attribute_names].each_with_index do |attr, index|
|
241
|
+
attributes[attr] = args[index]
|
242
|
+
end
|
243
|
+
|
244
|
+
result = find(options[:finder], find_options)
|
245
|
+
|
246
|
+
if result.nil?
|
247
|
+
if options[:bang]
|
248
|
+
raise DocumentNotFound, "Couldn't find Document with #{attributes.inspect} in collection named #{collection.name}"
|
249
|
+
end
|
250
|
+
|
251
|
+
if options[:instantiator]
|
252
|
+
self.send(options[:instantiator], attributes)
|
253
|
+
end
|
254
|
+
else
|
255
|
+
result
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def update_single(id, attrs)
|
260
|
+
if id.blank? || attrs.blank? || !attrs.is_a?(Hash)
|
261
|
+
raise ArgumentError, "Updating a single document requires an id and a hash of attributes"
|
262
|
+
end
|
263
|
+
|
264
|
+
doc = find(id)
|
265
|
+
doc.update_attributes(attrs)
|
266
|
+
doc
|
267
|
+
end
|
268
|
+
|
269
|
+
def update_multiple(docs)
|
270
|
+
unless docs.is_a?(Hash)
|
271
|
+
raise ArgumentError, "Updating multiple documents takes 1 argument and it must be hash"
|
272
|
+
end
|
273
|
+
|
274
|
+
instances = []
|
275
|
+
docs.each_pair { |id, attrs| instances << update(id, attrs) }
|
276
|
+
instances
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
module InstanceMethods
|
281
|
+
def collection
|
282
|
+
self.class.collection
|
283
|
+
end
|
284
|
+
|
285
|
+
def new?
|
286
|
+
read_attribute('_id').blank? || using_custom_id?
|
287
|
+
end
|
288
|
+
|
289
|
+
def save
|
290
|
+
create_or_update
|
291
|
+
end
|
292
|
+
|
293
|
+
def save!
|
294
|
+
create_or_update || raise(DocumentNotValid.new(self))
|
295
|
+
end
|
296
|
+
|
297
|
+
def destroy
|
298
|
+
return false if frozen?
|
299
|
+
|
300
|
+
criteria = FinderOptions.to_mongo_criteria(:_id => id)
|
301
|
+
collection.remove(criteria) unless new?
|
302
|
+
freeze
|
303
|
+
end
|
304
|
+
|
305
|
+
private
|
306
|
+
def create_or_update
|
307
|
+
result = new? ? create : update
|
308
|
+
result != false
|
309
|
+
end
|
310
|
+
|
311
|
+
def create
|
312
|
+
assign_id
|
313
|
+
save_to_collection
|
314
|
+
end
|
315
|
+
|
316
|
+
def assign_id
|
317
|
+
if read_attribute(:_id).blank?
|
318
|
+
write_attribute(:_id, Mongo::ObjectID.new.to_s)
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
def update
|
323
|
+
save_to_collection
|
324
|
+
end
|
325
|
+
|
326
|
+
def save_to_collection
|
327
|
+
clear_custom_id_flag
|
328
|
+
collection.save(to_mongo)
|
329
|
+
end
|
330
|
+
|
331
|
+
def update_timestamps
|
332
|
+
now = Time.now.utc
|
333
|
+
write_attribute('created_at', now) if new?
|
334
|
+
write_attribute('updated_at', now)
|
335
|
+
end
|
336
|
+
|
337
|
+
def clear_custom_id_flag
|
338
|
+
@using_custom_id = nil
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end # Document
|
342
|
+
end # MongoMapper
|