mongomodel 0.1.6 → 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/README.md +6 -0
- data/bin/console +4 -4
- data/lib/mongomodel.rb +4 -4
- data/lib/mongomodel/concerns/associations/base/definition.rb +4 -0
- data/lib/mongomodel/concerns/associations/has_many_by_foreign_key.rb +7 -16
- data/lib/mongomodel/concerns/associations/has_many_by_ids.rb +6 -12
- data/lib/mongomodel/concerns/attributes.rb +1 -7
- data/lib/mongomodel/document.rb +0 -1
- data/lib/mongomodel/document/dynamic_finders.rb +2 -69
- data/lib/mongomodel/document/indexes.rb +0 -6
- data/lib/mongomodel/document/persistence.rb +1 -21
- data/lib/mongomodel/document/scopes.rb +59 -135
- data/lib/mongomodel/document/validations/uniqueness.rb +7 -5
- data/lib/mongomodel/support/dynamic_finder.rb +68 -0
- data/lib/mongomodel/support/mongo_operator.rb +29 -0
- data/lib/mongomodel/support/mongo_options.rb +0 -101
- data/lib/mongomodel/support/mongo_order.rb +78 -0
- data/lib/mongomodel/support/scope.rb +186 -0
- data/lib/mongomodel/support/scope/dynamic_finders.rb +21 -0
- data/lib/mongomodel/support/scope/finder_methods.rb +61 -0
- data/lib/mongomodel/support/scope/query_methods.rb +43 -0
- data/lib/mongomodel/support/scope/spawn_methods.rb +35 -0
- data/lib/mongomodel/version.rb +1 -1
- data/mongomodel.gemspec +20 -3
- data/spec/mongomodel/concerns/associations/has_many_by_foreign_key_spec.rb +1 -1
- data/spec/mongomodel/concerns/associations/has_many_by_ids_spec.rb +1 -1
- data/spec/mongomodel/document/dynamic_finders_spec.rb +0 -1
- data/spec/mongomodel/document/finders_spec.rb +0 -144
- data/spec/mongomodel/document/indexes_spec.rb +2 -2
- data/spec/mongomodel/document/persistence_spec.rb +1 -15
- data/spec/mongomodel/document/scopes_spec.rb +64 -167
- data/spec/mongomodel/support/mongo_operator_spec.rb +29 -0
- data/spec/mongomodel/support/mongo_options_spec.rb +0 -150
- data/spec/mongomodel/support/mongo_order_spec.rb +127 -0
- data/spec/mongomodel/support/scope_spec.rb +932 -0
- data/spec/support/helpers/document_finder_stubs.rb +40 -0
- data/spec/support/matchers/find_with.rb +36 -0
- metadata +22 -5
- data/lib/mongomodel/document/finders.rb +0 -82
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module MongoModel
|
4
|
+
describe MongoOrder do
|
5
|
+
def c(field, order)
|
6
|
+
MongoOrder::Clause.new(field, order)
|
7
|
+
end
|
8
|
+
|
9
|
+
subject { MongoOrder.new(c(:name, :ascending), c(:age, :descending)) }
|
10
|
+
|
11
|
+
it "should convert to string" do
|
12
|
+
subject.to_s.should == "name ascending, age descending"
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "#to_sort" do
|
16
|
+
it "should convert to mongo sort array" do
|
17
|
+
model = mock('model', :properties => mock('properties', :[] => nil))
|
18
|
+
subject.to_sort(model).should == [['name', :ascending], ['age', :descending]]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should be reversable" do
|
23
|
+
subject.reverse.should == MongoOrder.new(c(:name, :descending), c(:age, :ascending))
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should equal another order object with identical clauses" do
|
27
|
+
subject.should == MongoOrder.new(c(:name, :ascending), c(:age, :descending))
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should equal another order object with different clauses" do
|
31
|
+
subject.should_not == MongoOrder.new(c(:name, :ascending))
|
32
|
+
subject.should_not == MongoOrder.new(c(:age, :ascending), c(:name, :ascending))
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "#parse" do
|
36
|
+
it "should not change a MongoOrder" do
|
37
|
+
MongoOrder.parse(subject).should == subject
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should convert individual clause to MongoOrder" do
|
41
|
+
MongoOrder.parse(c(:name, :ascending)).should == MongoOrder.new(c(:name, :ascending))
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should convert symbol to MongoOrder" do
|
45
|
+
MongoOrder.parse(:name).should == MongoOrder.new(c(:name, :ascending))
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should convert array of clauses to MongoOrder" do
|
49
|
+
MongoOrder.parse([c(:name, :ascending), c(:age, :descending)]).should == MongoOrder.new(c(:name, :ascending), c(:age, :descending))
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should convert array of symbols to MongoOrder" do
|
53
|
+
MongoOrder.parse([:name, :age]).should == MongoOrder.new(c(:name, :ascending), c(:age, :ascending))
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should convert array of strings to MongoOrder" do
|
57
|
+
MongoOrder.parse(['name ASC', 'age DESC']).should == MongoOrder.new(c(:name, :ascending), c(:age, :descending))
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should convert string (no order specified) to MongoOrder" do
|
61
|
+
MongoOrder.parse('name').should == MongoOrder.new(c(:name, :ascending))
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should convert string (single order) to MongoOrder" do
|
65
|
+
MongoOrder.parse('name DESC').should == MongoOrder.new(c(:name, :descending))
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should convert string (multiple orders) to MongoOrder" do
|
69
|
+
MongoOrder.parse('name DESC, age ASC').should == MongoOrder.new(c(:name, :descending), c(:age, :ascending))
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe MongoOrder::Clause do
|
75
|
+
subject { MongoOrder::Clause.new(:name, :ascending) }
|
76
|
+
|
77
|
+
it "should convert to string" do
|
78
|
+
subject.to_s.should == "name ascending"
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should equal another clause with the same field and order" do
|
82
|
+
subject.should == MongoOrder::Clause.new(:name, :ascending)
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should equal another clause with a different field or order" do
|
86
|
+
subject.should_not == MongoOrder::Clause.new(:age, :ascending)
|
87
|
+
subject.should_not == MongoOrder::Clause.new(:name, :descending)
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should be reversable" do
|
91
|
+
subject.reverse.should == MongoOrder::Clause.new(:name, :descending)
|
92
|
+
end
|
93
|
+
|
94
|
+
describe "#to_sort" do
|
95
|
+
context "given property" do
|
96
|
+
it "should use property as value to convert to mongo sort" do
|
97
|
+
property = mock('property', :as => '_name')
|
98
|
+
subject.to_sort(property).should == ['_name', :ascending]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context "given nil" do
|
103
|
+
it "should convert to mongo sort" do
|
104
|
+
subject.to_sort(nil).should == ['name', :ascending]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe "#parse" do
|
110
|
+
let(:asc) { MongoOrder::Clause.new(:name, :ascending) }
|
111
|
+
let(:desc) { MongoOrder::Clause.new(:name, :descending) }
|
112
|
+
|
113
|
+
it "should create Clause from string (no order)" do
|
114
|
+
MongoOrder::Clause.parse('name').should == asc
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should create Clause from string (with order)" do
|
118
|
+
MongoOrder::Clause.parse('name ASC').should == asc
|
119
|
+
MongoOrder::Clause.parse('name asc').should == asc
|
120
|
+
MongoOrder::Clause.parse('name ascending').should == asc
|
121
|
+
MongoOrder::Clause.parse('name DESC').should == desc
|
122
|
+
MongoOrder::Clause.parse('name desc').should == desc
|
123
|
+
MongoOrder::Clause.parse('name descending').should == desc
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,932 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module MongoModel
|
4
|
+
describe Scope do
|
5
|
+
define_class(:Post, Document) do
|
6
|
+
property :published, Boolean, :default => false
|
7
|
+
property :author, String
|
8
|
+
property :date, Time
|
9
|
+
end
|
10
|
+
|
11
|
+
let(:basic_scope) { Scope.new(Post) }
|
12
|
+
let(:posts) { (1..5).map { Post.new } }
|
13
|
+
|
14
|
+
subject { Scope.new(Post) }
|
15
|
+
|
16
|
+
MongoModel::Document.extend(DocumentFinderStubs)
|
17
|
+
|
18
|
+
|
19
|
+
def self.subject_loaded(&block)
|
20
|
+
context "when loaded" do
|
21
|
+
before(:each) { subject.to_a }
|
22
|
+
class_eval(&block)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.subject_not_loaded(&block)
|
27
|
+
context "when not loaded", &block
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.always(&block)
|
31
|
+
subject_loaded(&block)
|
32
|
+
subject_not_loaded(&block)
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
shared_examples_for "all scopes" do
|
37
|
+
def finder_conditions
|
38
|
+
finder_options[:conditions] || {}
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "#to_a" do
|
42
|
+
it "should find and return documents matching conditions" do
|
43
|
+
model.should_find(finder_options, posts) do
|
44
|
+
subject.to_a.should == posts
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should load the scope" do
|
49
|
+
subject.to_a
|
50
|
+
subject.should be_loaded
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should cache the documents" do
|
54
|
+
subject.to_a
|
55
|
+
model.should_not_find { subject.to_a }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "#count" do
|
60
|
+
it "should count documents matching conditions and return the result" do
|
61
|
+
model.should_count(finder_options, 4) do
|
62
|
+
subject.count
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should not cache the result" do
|
67
|
+
subject.count
|
68
|
+
model.should_count(finder_options, 4) do
|
69
|
+
subject.count.should == 4
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "#size" do
|
75
|
+
before(:each) { model.stub_find(posts) }
|
76
|
+
|
77
|
+
subject_loaded do
|
78
|
+
it "should return the number of matching documents" do
|
79
|
+
subject.size.should == 5
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should not perform a count on the collection" do
|
83
|
+
model.should_not_count { subject.size }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
subject_not_loaded do
|
88
|
+
it "should return the number of matching documents" do
|
89
|
+
subject.size.should == 5
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should perform a count on the collection" do
|
93
|
+
model.should_count(finder_options, 9) do
|
94
|
+
subject.size.should == 9
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe "#empty?" do
|
101
|
+
context "when no matching documents exist" do
|
102
|
+
before(:each) { model.stub_find([]) }
|
103
|
+
|
104
|
+
always do
|
105
|
+
it { should be_empty }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context "when matching documents exist" do
|
110
|
+
before(:each) { model.stub_find(posts) }
|
111
|
+
|
112
|
+
always do
|
113
|
+
it { should_not be_empty }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
subject_loaded do
|
118
|
+
it "should not perform a count on the collection" do
|
119
|
+
model.should_not_count { subject.empty? }
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe "#any?" do
|
125
|
+
context "when no block given" do
|
126
|
+
it "should perform a count on the collection" do
|
127
|
+
model.should_count(finder_options, 1) { subject.any? }
|
128
|
+
end
|
129
|
+
|
130
|
+
context "when no matching documents exist" do
|
131
|
+
before(:each) { model.stub_find([]) }
|
132
|
+
|
133
|
+
always do
|
134
|
+
specify { subject.any?.should be_false }
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
context "when matching documents exist" do
|
139
|
+
before(:each) { model.stub_find(posts) }
|
140
|
+
|
141
|
+
always do
|
142
|
+
specify { subject.any?.should be_true }
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
context "when block given" do
|
148
|
+
it "should delegate block to to_a" do
|
149
|
+
blk = lambda { |*args| true }
|
150
|
+
subject.to_a.should_receive(:any?).with(&blk)
|
151
|
+
subject.any?(&blk)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
describe "#reset" do
|
157
|
+
always do
|
158
|
+
it "should return itself" do
|
159
|
+
subject.reset.should equal(subject)
|
160
|
+
end
|
161
|
+
|
162
|
+
it "should not be loaded" do
|
163
|
+
subject.reset
|
164
|
+
subject.should_not be_loaded
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
describe "#reload" do
|
170
|
+
always do
|
171
|
+
it "should return itself" do
|
172
|
+
subject.reload.should equal(subject)
|
173
|
+
end
|
174
|
+
|
175
|
+
it "should be loaded after calling" do
|
176
|
+
subject.reload
|
177
|
+
subject.should be_loaded
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
describe "#==" do
|
183
|
+
before(:each) { model.stub_find(posts) }
|
184
|
+
|
185
|
+
it "should be equal to an array with matching results" do
|
186
|
+
subject.should == posts
|
187
|
+
end
|
188
|
+
|
189
|
+
it "should not be equal to an array with different results" do
|
190
|
+
subject.should_not == []
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
describe "array methods" do
|
195
|
+
it "should forward [] to to_a" do
|
196
|
+
subject.to_a.should_receive(:[]).with(0)
|
197
|
+
subject[0]
|
198
|
+
end
|
199
|
+
|
200
|
+
it "should forward each to to_a" do
|
201
|
+
blk = lambda { |*args| true }
|
202
|
+
subject.to_a.should_receive(:each).with(&blk)
|
203
|
+
subject.each(&blk)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
describe "#where" do
|
208
|
+
it "should return a new scope" do
|
209
|
+
subject.where(:author => "Sam").should be_an_instance_of(Scope)
|
210
|
+
end
|
211
|
+
|
212
|
+
it "should not be loaded" do
|
213
|
+
subject.to_a
|
214
|
+
subject.where(:author => "Sam").should_not be_loaded
|
215
|
+
end
|
216
|
+
|
217
|
+
it "should add individual where values" do
|
218
|
+
where_scope = subject.where(:author => "Sam")
|
219
|
+
where_scope.where_values.should == subject.where_values + [{ :author => "Sam" }]
|
220
|
+
end
|
221
|
+
|
222
|
+
it "should add multiple where values" do
|
223
|
+
where_scope = subject.where({ :author => "Sam" }, { :published => false })
|
224
|
+
where_scope.where_values.should == subject.where_values + [{ :author => "Sam" }, { :published => false }]
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
describe "#order" do
|
229
|
+
it "should return a new scope" do
|
230
|
+
subject.order(:author.asc).should be_an_instance_of(Scope)
|
231
|
+
end
|
232
|
+
|
233
|
+
it "should not be loaded" do
|
234
|
+
subject.to_a
|
235
|
+
subject.order(:author.asc).should_not be_loaded
|
236
|
+
end
|
237
|
+
|
238
|
+
it "should add individual order values" do
|
239
|
+
order_scope = subject.order(:author.asc)
|
240
|
+
order_scope.order_values.should == subject.order_values + [:author.asc]
|
241
|
+
end
|
242
|
+
|
243
|
+
it "should add multiple order values" do
|
244
|
+
order_scope = subject.order(:author.asc, :published.desc)
|
245
|
+
order_scope.order_values.should == subject.order_values + [:author.asc, :published.desc]
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
describe "#select" do
|
250
|
+
it "should return a new scope" do
|
251
|
+
subject.select(:author).should be_an_instance_of(Scope)
|
252
|
+
end
|
253
|
+
|
254
|
+
it "should not be loaded" do
|
255
|
+
subject.to_a
|
256
|
+
subject.select(:author).should_not be_loaded
|
257
|
+
end
|
258
|
+
|
259
|
+
it "should add individual select values" do
|
260
|
+
select_scope = subject.select(:author)
|
261
|
+
select_scope.select_values.should == subject.select_values + [:author]
|
262
|
+
end
|
263
|
+
|
264
|
+
it "should add multiple select values" do
|
265
|
+
select_scope = subject.select(:author, :published)
|
266
|
+
select_scope.select_values.should == subject.select_values + [:author, :published]
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
describe "#limit" do
|
271
|
+
it "should return a new scope" do
|
272
|
+
subject.limit(10).should be_an_instance_of(Scope)
|
273
|
+
end
|
274
|
+
|
275
|
+
it "should not be loaded" do
|
276
|
+
subject.limit(10).should_not be_loaded
|
277
|
+
end
|
278
|
+
|
279
|
+
it "should override previous limit value" do
|
280
|
+
subject.limit(10).limit_value.should == 10
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
describe "#offset" do
|
285
|
+
it "should return a new scope" do
|
286
|
+
subject.offset(10).should be_an_instance_of(Scope)
|
287
|
+
end
|
288
|
+
|
289
|
+
it "should not be loaded" do
|
290
|
+
subject.offset(10).should_not be_loaded
|
291
|
+
end
|
292
|
+
|
293
|
+
it "should override previous offset value" do
|
294
|
+
subject.offset(10).offset_value.should == 10
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
describe "#from" do
|
299
|
+
define_class(:NotAPost, Document)
|
300
|
+
|
301
|
+
it "should return a new scope" do
|
302
|
+
subject.from(NotAPost.collection).should be_an_instance_of(Scope)
|
303
|
+
end
|
304
|
+
|
305
|
+
it "should not be loaded" do
|
306
|
+
subject.from(NotAPost.collection).should_not be_loaded
|
307
|
+
end
|
308
|
+
|
309
|
+
it "should override collection" do
|
310
|
+
subject.from(NotAPost.collection).collection.should == NotAPost.collection
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
describe "#first" do
|
315
|
+
context "when no matching documents exist" do
|
316
|
+
before(:each) { model.stub_find([]) }
|
317
|
+
|
318
|
+
always do
|
319
|
+
it "should return nil" do
|
320
|
+
subject.first.should be_nil
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
subject_loaded do
|
325
|
+
it "should not perform a find" do
|
326
|
+
model.should_not_find do
|
327
|
+
subject.first
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
subject_not_loaded do
|
333
|
+
it "should find with a limit of 1" do
|
334
|
+
model.should_find(finder_options.merge(:limit => 1), []) do
|
335
|
+
subject.first
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
context "when matching documents exist" do
|
342
|
+
let(:post) { posts.first }
|
343
|
+
before(:each) { model.stub_find([post]) }
|
344
|
+
|
345
|
+
always do
|
346
|
+
it "should return the first document" do
|
347
|
+
subject.first.should == post
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
subject_not_loaded do
|
352
|
+
it "should cache find result" do
|
353
|
+
model.should_find(finder_options.merge(:limit => 1), [post]) do
|
354
|
+
subject.first
|
355
|
+
subject.first
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
describe "#last" do
|
363
|
+
def reversed_finder_options
|
364
|
+
order = MongoModel::MongoOrder.parse(finder_options[:order] || [:id.asc])
|
365
|
+
finder_options.merge(:order => order.reverse.to_a)
|
366
|
+
end
|
367
|
+
|
368
|
+
context "when no matching documents exist" do
|
369
|
+
before(:each) { model.stub_find([]) }
|
370
|
+
|
371
|
+
always do
|
372
|
+
it "should return nil" do
|
373
|
+
subject.last.should be_nil
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
subject_loaded do
|
378
|
+
it "should not perform a find" do
|
379
|
+
model.should_not_find do
|
380
|
+
subject.last
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
subject_not_loaded do
|
386
|
+
it "should find with a limit of 1" do
|
387
|
+
model.should_find(reversed_finder_options.merge(:limit => 1), []) do
|
388
|
+
subject.last
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
context "when matching documents exist" do
|
395
|
+
let(:post) { posts.last }
|
396
|
+
before(:each) { model.stub_find([post]) }
|
397
|
+
|
398
|
+
always do
|
399
|
+
it "should return the last document" do
|
400
|
+
subject.last.should == post
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
subject_not_loaded do
|
405
|
+
it "should cache find result" do
|
406
|
+
model.should_find(reversed_finder_options.merge(:limit => 1), [post]) do
|
407
|
+
subject.last
|
408
|
+
subject.last
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
describe "#all" do
|
416
|
+
it "should return all documents" do
|
417
|
+
model.should_find(finder_options, posts) do
|
418
|
+
subject.all.should == posts
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
describe "#find" do
|
424
|
+
context "with single id" do
|
425
|
+
let(:post) { posts.first }
|
426
|
+
|
427
|
+
it "should perform find on collection" do
|
428
|
+
model.should_find(finder_options.deep_merge(:conditions => { :id => post.id }, :limit => 1), [post]) do
|
429
|
+
subject.find(post.id)
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
context "when document exists" do
|
434
|
+
before(:each) { model.stub_find([post]) }
|
435
|
+
|
436
|
+
it "should find and return document" do
|
437
|
+
subject.find(post.id).should == post
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
context "when document does not exist" do
|
442
|
+
before(:each) { model.stub_find([]) }
|
443
|
+
|
444
|
+
it "should raise a DocumentNotFound exception" do
|
445
|
+
lambda {
|
446
|
+
subject.find('missing')
|
447
|
+
}.should raise_error(MongoModel::DocumentNotFound)
|
448
|
+
end
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
context "by multiple ids" do
|
453
|
+
let(:post1) { posts.first }
|
454
|
+
let(:post2) { posts.last }
|
455
|
+
|
456
|
+
it "should perform find on collection" do
|
457
|
+
model.should_find(finder_options.deep_merge(:conditions => { :id.in => [post2.id, post1.id] }), [post1, post2]) do
|
458
|
+
subject.find(post2.id, post1.id)
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
context "when all documents exist" do
|
463
|
+
before(:each) { model.stub_find([post2, post1]) }
|
464
|
+
|
465
|
+
it "should return documents in order given" do
|
466
|
+
subject.find(post2.id, post1.id).should == [post2, post1]
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
context "when some documents do not exist" do
|
471
|
+
before(:each) { model.stub_find([post1]) }
|
472
|
+
|
473
|
+
it "should raise a DocumentNotFound exception" do
|
474
|
+
lambda {
|
475
|
+
subject.find(post1.id, 'missing')
|
476
|
+
}.should raise_error(MongoModel::DocumentNotFound)
|
477
|
+
end
|
478
|
+
end
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
describe "#exists?" do
|
483
|
+
let(:post) { posts.first }
|
484
|
+
|
485
|
+
it "should perform a count on the collection" do
|
486
|
+
model.should_count(finder_options.deep_merge(:conditions => { :id => post.id }), 1) do
|
487
|
+
subject.exists?(post.id)
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
context "when the document exists" do
|
492
|
+
before(:each) { model.stub_find([post])}
|
493
|
+
|
494
|
+
it "should return true" do
|
495
|
+
subject.exists?(post.id).should be_true
|
496
|
+
end
|
497
|
+
end
|
498
|
+
|
499
|
+
context "when the document does not exist" do
|
500
|
+
before(:each) { model.stub_find([])}
|
501
|
+
|
502
|
+
it "should return false" do
|
503
|
+
subject.exists?('missing').should be_false
|
504
|
+
end
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
describe "#delete_all" do
|
509
|
+
it "should remove all matching documents from collection" do
|
510
|
+
model.should_delete(finder_conditions) do
|
511
|
+
subject.delete_all
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
515
|
+
subject_loaded do
|
516
|
+
it "should reset the scope" do
|
517
|
+
subject.delete_all
|
518
|
+
subject.should_not be_loaded
|
519
|
+
end
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
describe "#delete" do
|
524
|
+
context "by single id" do
|
525
|
+
it "should remove the document from the collection" do
|
526
|
+
model.should_delete(finder_conditions.merge(:id => "the-id")) do
|
527
|
+
subject.delete("the-id")
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
subject_loaded do
|
532
|
+
it "should reset the scope" do
|
533
|
+
subject.delete("the-id")
|
534
|
+
subject.should_not be_loaded
|
535
|
+
end
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
context "by multiple ids" do
|
540
|
+
it "should remove the document from the collection" do
|
541
|
+
model.should_delete(finder_conditions.merge(:id.in => ["first-id", "second-id"])) do
|
542
|
+
subject.delete("first-id", "second-id")
|
543
|
+
end
|
544
|
+
end
|
545
|
+
|
546
|
+
subject_loaded do
|
547
|
+
it "should reset the scope" do
|
548
|
+
subject.delete("first-id", "second-id")
|
549
|
+
subject.should_not be_loaded
|
550
|
+
end
|
551
|
+
end
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
describe "#destroy_all" do
|
556
|
+
let(:post1) { posts.first }
|
557
|
+
let(:post2) { posts.last }
|
558
|
+
|
559
|
+
before(:each) { model.stub_find([post1, post2]) }
|
560
|
+
|
561
|
+
it "should destroy all matching documents individually" do
|
562
|
+
Post.should_delete(:id => post1.id)
|
563
|
+
Post.should_delete(:id => post2.id)
|
564
|
+
subject.destroy_all
|
565
|
+
end
|
566
|
+
|
567
|
+
subject_loaded do
|
568
|
+
it "should reset the scope" do
|
569
|
+
subject.destroy_all
|
570
|
+
subject.should_not be_loaded
|
571
|
+
end
|
572
|
+
end
|
573
|
+
end
|
574
|
+
|
575
|
+
describe "#destroy" do
|
576
|
+
context "by single id" do
|
577
|
+
let(:post) { posts.first }
|
578
|
+
|
579
|
+
before(:each) { model.stub_find([post]) }
|
580
|
+
|
581
|
+
it "should destroy the retrieved document" do
|
582
|
+
Post.should_delete(:id => post.id)
|
583
|
+
subject.destroy(post.id)
|
584
|
+
end
|
585
|
+
|
586
|
+
subject_loaded do
|
587
|
+
it "should reset the scope" do
|
588
|
+
subject.destroy(post.id)
|
589
|
+
subject.should_not be_loaded
|
590
|
+
end
|
591
|
+
end
|
592
|
+
end
|
593
|
+
|
594
|
+
context "by multiple ids" do
|
595
|
+
let(:post1) { posts.first }
|
596
|
+
let(:post2) { posts.last }
|
597
|
+
|
598
|
+
before(:each) { model.stub_find([post1, post2]) }
|
599
|
+
|
600
|
+
it "should destroy the documents individually" do
|
601
|
+
Post.should_delete(:id => post1.id)
|
602
|
+
Post.should_delete(:id => post2.id)
|
603
|
+
subject.destroy(post1.id, post2.id)
|
604
|
+
end
|
605
|
+
|
606
|
+
subject_loaded do
|
607
|
+
it "should reset the scope" do
|
608
|
+
subject.destroy(post1.id, post2.id)
|
609
|
+
subject.should_not be_loaded
|
610
|
+
end
|
611
|
+
end
|
612
|
+
end
|
613
|
+
end
|
614
|
+
end
|
615
|
+
|
616
|
+
|
617
|
+
context "without criteria" do
|
618
|
+
subject { basic_scope }
|
619
|
+
|
620
|
+
context "when initialized" do
|
621
|
+
it { should_not be_loaded }
|
622
|
+
end
|
623
|
+
|
624
|
+
context "when loaded" do
|
625
|
+
before(:each) { subject.to_a }
|
626
|
+
its(:clone) { should_not be_loaded }
|
627
|
+
end
|
628
|
+
|
629
|
+
def model
|
630
|
+
Post
|
631
|
+
end
|
632
|
+
|
633
|
+
def finder_options
|
634
|
+
{}
|
635
|
+
end
|
636
|
+
|
637
|
+
it_should_behave_like "all scopes"
|
638
|
+
|
639
|
+
it "should use collection from class" do
|
640
|
+
subject.collection.should == Post.collection
|
641
|
+
end
|
642
|
+
|
643
|
+
describe "#inspect" do
|
644
|
+
before(:each) { Post.stub_find(posts) }
|
645
|
+
|
646
|
+
it "should delegate to to_a" do
|
647
|
+
subject.inspect.should == posts.inspect
|
648
|
+
end
|
649
|
+
end
|
650
|
+
|
651
|
+
describe "#==" do
|
652
|
+
define_class(:NotAPost, Document)
|
653
|
+
|
654
|
+
it "should be equal to a new scope for the same class" do
|
655
|
+
subject.should == Scope.new(Post)
|
656
|
+
end
|
657
|
+
|
658
|
+
it "should not be equal to a scope for a different class" do
|
659
|
+
subject.should_not == Scope.new(NotAPost)
|
660
|
+
end
|
661
|
+
end
|
662
|
+
|
663
|
+
describe "#reverse_order" do
|
664
|
+
subject { basic_scope.reverse_order }
|
665
|
+
|
666
|
+
it "should return a new scope" do
|
667
|
+
subject.should be_an_instance_of(Scope)
|
668
|
+
end
|
669
|
+
|
670
|
+
it "should not be loaded" do
|
671
|
+
basic_scope.to_a # Load parent scope
|
672
|
+
subject.should_not be_loaded
|
673
|
+
end
|
674
|
+
|
675
|
+
it "should set the order value to descending by id" do
|
676
|
+
subject.order_values.should == [:id.desc]
|
677
|
+
end
|
678
|
+
end
|
679
|
+
|
680
|
+
describe "#build" do
|
681
|
+
it "should return a new document" do
|
682
|
+
subject.build.should be_an_instance_of(Post)
|
683
|
+
end
|
684
|
+
|
685
|
+
it "should be aliased as #new" do
|
686
|
+
subject.new(:id => '123').should == subject.build(:id => '123')
|
687
|
+
end
|
688
|
+
end
|
689
|
+
|
690
|
+
describe "#create" do
|
691
|
+
it "should return a new document" do
|
692
|
+
subject.create.should be_an_instance_of(Post)
|
693
|
+
end
|
694
|
+
|
695
|
+
it "should save the document" do
|
696
|
+
subject.create.should_not be_a_new_record
|
697
|
+
end
|
698
|
+
end
|
699
|
+
|
700
|
+
describe "#apply_finder_options" do
|
701
|
+
it "should return a new scope" do
|
702
|
+
subject.apply_finder_options({}).should be_an_instance_of(Scope)
|
703
|
+
end
|
704
|
+
|
705
|
+
it "should set where values from options" do
|
706
|
+
scope = subject.apply_finder_options({ :conditions => { :author => "John" } })
|
707
|
+
scope.where_values.should == [{ :author => "John" }]
|
708
|
+
end
|
709
|
+
|
710
|
+
it "should set order values from options" do
|
711
|
+
scope = subject.apply_finder_options({ :order => :author.desc })
|
712
|
+
scope.order_values.should == [:author.desc]
|
713
|
+
end
|
714
|
+
|
715
|
+
it "should set select values from options" do
|
716
|
+
scope = subject.apply_finder_options({ :select => [:id, :author] })
|
717
|
+
scope.select_values.should == [:id, :author]
|
718
|
+
end
|
719
|
+
|
720
|
+
it "should set offset value from options" do
|
721
|
+
scope = subject.apply_finder_options({ :offset => 40 })
|
722
|
+
scope.offset_value.should == 40
|
723
|
+
end
|
724
|
+
|
725
|
+
it "should set limit value from options" do
|
726
|
+
scope = subject.apply_finder_options({ :limit => 50 })
|
727
|
+
scope.limit_value.should == 50
|
728
|
+
end
|
729
|
+
end
|
730
|
+
end
|
731
|
+
|
732
|
+
|
733
|
+
context "with criteria" do
|
734
|
+
define_class(:OtherPost, Document)
|
735
|
+
|
736
|
+
let(:timestamp) { Time.now }
|
737
|
+
let(:scoped) do
|
738
|
+
basic_scope.where(:author => "Sam").
|
739
|
+
where(:published => true).
|
740
|
+
where(:date.lt => timestamp).
|
741
|
+
order(:author.asc).
|
742
|
+
order(:published.desc).
|
743
|
+
select(:author).
|
744
|
+
select(:published).
|
745
|
+
offset(15).
|
746
|
+
limit(7).
|
747
|
+
from(OtherPost.collection)
|
748
|
+
end
|
749
|
+
|
750
|
+
subject { scoped }
|
751
|
+
|
752
|
+
def model
|
753
|
+
OtherPost
|
754
|
+
end
|
755
|
+
|
756
|
+
def finder_options
|
757
|
+
{
|
758
|
+
:conditions => { "author" => "Sam", "published" => true, "date" => { "$lt" => timestamp } },
|
759
|
+
:order => [:author.asc, :published.desc],
|
760
|
+
:select => [:author, :published],
|
761
|
+
:offset => 15,
|
762
|
+
:limit => 7
|
763
|
+
}
|
764
|
+
end
|
765
|
+
|
766
|
+
it_should_behave_like "all scopes"
|
767
|
+
|
768
|
+
describe "#build" do
|
769
|
+
it "should use equality where conditions as attributes" do
|
770
|
+
doc = subject.build
|
771
|
+
doc.author.should == "Sam"
|
772
|
+
doc.published.should be_true
|
773
|
+
doc.date.should be_nil
|
774
|
+
end
|
775
|
+
end
|
776
|
+
|
777
|
+
describe "#create" do
|
778
|
+
it "should use equality where conditions as attributes" do
|
779
|
+
doc = subject.create
|
780
|
+
doc.author.should == "Sam"
|
781
|
+
doc.published.should be_true
|
782
|
+
doc.date.should be_nil
|
783
|
+
end
|
784
|
+
end
|
785
|
+
|
786
|
+
describe "#reverse_order" do
|
787
|
+
subject { scoped.reverse_order }
|
788
|
+
|
789
|
+
it "should set the order values to the reverse order" do
|
790
|
+
subject.order_values.should == [:author.desc, :published.asc]
|
791
|
+
end
|
792
|
+
end
|
793
|
+
|
794
|
+
describe "#except" do
|
795
|
+
context "given :where" do
|
796
|
+
it "should return a new scope without where values" do
|
797
|
+
s = subject.except(:where)
|
798
|
+
s.where_values.should be_empty
|
799
|
+
s.order_values.should == [:author.asc, :published.desc]
|
800
|
+
s.select_values.should == [:author, :published]
|
801
|
+
s.offset_value.should == 15
|
802
|
+
s.limit_value.should == 7
|
803
|
+
s.collection.should == OtherPost.collection
|
804
|
+
end
|
805
|
+
end
|
806
|
+
|
807
|
+
context "given :order" do
|
808
|
+
it "should return a new scope without order values" do
|
809
|
+
s = subject.except(:order)
|
810
|
+
s.where_values.should == [{ :author => "Sam" }, { :published => true }, { :date.lt => timestamp }]
|
811
|
+
s.order_values.should be_empty
|
812
|
+
s.select_values.should == [:author, :published]
|
813
|
+
s.offset_value.should == 15
|
814
|
+
s.limit_value.should == 7
|
815
|
+
s.collection.should == OtherPost.collection
|
816
|
+
end
|
817
|
+
end
|
818
|
+
|
819
|
+
context "given :select" do
|
820
|
+
it "should return a new scope without select values" do
|
821
|
+
s = subject.except(:select)
|
822
|
+
s.where_values.should == [{ :author => "Sam" }, { :published => true }, { :date.lt => timestamp }]
|
823
|
+
s.order_values.should == [:author.asc, :published.desc]
|
824
|
+
s.select_values.should be_empty
|
825
|
+
s.offset_value.should == 15
|
826
|
+
s.limit_value.should == 7
|
827
|
+
s.collection.should == OtherPost.collection
|
828
|
+
end
|
829
|
+
end
|
830
|
+
|
831
|
+
context "given :offset" do
|
832
|
+
it "should return a new scope without offset value" do
|
833
|
+
s = subject.except(:offset)
|
834
|
+
s.where_values.should == [{ :author => "Sam" }, { :published => true }, { :date.lt => timestamp }]
|
835
|
+
s.order_values.should == [:author.asc, :published.desc]
|
836
|
+
s.select_values.should == [:author, :published]
|
837
|
+
s.offset_value.should be_nil
|
838
|
+
s.limit_value.should == 7
|
839
|
+
s.collection.should == OtherPost.collection
|
840
|
+
end
|
841
|
+
end
|
842
|
+
|
843
|
+
context "given :limit" do
|
844
|
+
it "should return a new scope without limit value" do
|
845
|
+
s = subject.except(:limit)
|
846
|
+
s.where_values.should == [{ :author => "Sam" }, { :published => true }, { :date.lt => timestamp }]
|
847
|
+
s.order_values.should == [:author.asc, :published.desc]
|
848
|
+
s.select_values.should == [:author, :published]
|
849
|
+
s.offset_value.should == 15
|
850
|
+
s.limit_value.should be_nil
|
851
|
+
s.collection.should == OtherPost.collection
|
852
|
+
end
|
853
|
+
end
|
854
|
+
|
855
|
+
context "given :from" do
|
856
|
+
it "should return a new scope with default collection" do
|
857
|
+
s = subject.except(:from)
|
858
|
+
s.where_values.should == [{ :author => "Sam" }, { :published => true }, { :date.lt => timestamp }]
|
859
|
+
s.order_values.should == [:author.asc, :published.desc]
|
860
|
+
s.select_values.should == [:author, :published]
|
861
|
+
s.offset_value.should == 15
|
862
|
+
s.limit_value.should == 7
|
863
|
+
s.collection.should == Post.collection
|
864
|
+
end
|
865
|
+
end
|
866
|
+
end
|
867
|
+
|
868
|
+
describe "#merge" do
|
869
|
+
let(:merged) do
|
870
|
+
basic_scope.where(:date.gt => timestamp-1.year).
|
871
|
+
order(:date.desc).
|
872
|
+
select(:date)
|
873
|
+
end
|
874
|
+
let(:result) { subject.merge(merged) }
|
875
|
+
|
876
|
+
it "should combine where values from scopes" do
|
877
|
+
result.where_values.should == [
|
878
|
+
{ :author => "Sam" },
|
879
|
+
{ :published => true },
|
880
|
+
{ :date.lt => timestamp },
|
881
|
+
{ :date.gt => timestamp-1.year }
|
882
|
+
]
|
883
|
+
end
|
884
|
+
|
885
|
+
it "should combine order values from scopes" do
|
886
|
+
result.order_values.should == [:author.asc, :published.desc, :date.desc]
|
887
|
+
end
|
888
|
+
|
889
|
+
it "should combine select values from scopes" do
|
890
|
+
result.select_values.should == [:author, :published, :date]
|
891
|
+
end
|
892
|
+
|
893
|
+
context "merged scope has offset value" do
|
894
|
+
let(:merged) { basic_scope.offset(10) }
|
895
|
+
|
896
|
+
it "should use offset value from merged scope" do
|
897
|
+
result.offset_value.should == 10
|
898
|
+
end
|
899
|
+
end
|
900
|
+
|
901
|
+
context "merged scope has no offset value set" do
|
902
|
+
let(:merged) { basic_scope }
|
903
|
+
|
904
|
+
it "should use offset value from original scope" do
|
905
|
+
result.offset_value.should == 15
|
906
|
+
end
|
907
|
+
end
|
908
|
+
|
909
|
+
context "merged scope has limit value" do
|
910
|
+
let(:merged) { basic_scope.limit(50) }
|
911
|
+
|
912
|
+
it "should use limit value from merged scope" do
|
913
|
+
result.limit_value.should == 50
|
914
|
+
end
|
915
|
+
end
|
916
|
+
|
917
|
+
context "merged scope has no limit value set" do
|
918
|
+
let(:merged) { basic_scope }
|
919
|
+
|
920
|
+
it "should use limit value from original scope" do
|
921
|
+
result.limit_value.should == 7
|
922
|
+
end
|
923
|
+
end
|
924
|
+
|
925
|
+
it "should use from value (collection) from merged scope" do
|
926
|
+
merged = basic_scope.from(Post.collection)
|
927
|
+
subject.merge(merged).collection.should == Post.collection
|
928
|
+
end
|
929
|
+
end
|
930
|
+
end
|
931
|
+
end
|
932
|
+
end
|