plucky 0.5.2 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/.bundle/config +4 -0
  2. data/.gitignore +3 -1
  3. data/.travis.yml +2 -2
  4. data/Gemfile +13 -3
  5. data/Guardfile +13 -0
  6. data/README.md +1 -1
  7. data/Rakefile +3 -17
  8. data/examples/query.rb +1 -1
  9. data/lib/plucky.rb +13 -0
  10. data/lib/plucky/criteria_hash.rb +85 -56
  11. data/lib/plucky/normalizers/criteria_hash_key.rb +17 -0
  12. data/lib/plucky/normalizers/criteria_hash_value.rb +83 -0
  13. data/lib/plucky/normalizers/fields_value.rb +26 -0
  14. data/lib/plucky/normalizers/integer.rb +19 -0
  15. data/lib/plucky/normalizers/options_hash_key.rb +23 -0
  16. data/lib/plucky/normalizers/options_hash_value.rb +85 -0
  17. data/lib/plucky/normalizers/sort_value.rb +55 -0
  18. data/lib/plucky/options_hash.rb +56 -85
  19. data/lib/plucky/pagination/decorator.rb +3 -2
  20. data/lib/plucky/pagination/paginator.rb +15 -6
  21. data/lib/plucky/query.rb +93 -51
  22. data/lib/plucky/version.rb +1 -1
  23. data/script/criteria_hash.rb +21 -0
  24. data/{test → spec}/helper.rb +12 -8
  25. data/spec/plucky/criteria_hash_spec.rb +166 -0
  26. data/spec/plucky/normalizers/criteria_hash_key_spec.rb +37 -0
  27. data/spec/plucky/normalizers/criteria_hash_value_spec.rb +193 -0
  28. data/spec/plucky/normalizers/fields_value_spec.rb +45 -0
  29. data/spec/plucky/normalizers/integer_spec.rb +24 -0
  30. data/spec/plucky/normalizers/options_hash_key_spec.rb +23 -0
  31. data/spec/plucky/normalizers/options_hash_value_spec.rb +99 -0
  32. data/spec/plucky/normalizers/sort_value_spec.rb +94 -0
  33. data/spec/plucky/options_hash_spec.rb +64 -0
  34. data/{test/plucky/pagination/test_decorator.rb → spec/plucky/pagination/decorator_spec.rb} +8 -10
  35. data/spec/plucky/pagination/paginator_spec.rb +118 -0
  36. data/spec/plucky/query_spec.rb +839 -0
  37. data/spec/plucky_spec.rb +68 -0
  38. data/{test/test_symbol_operator.rb → spec/symbol_operator_spec.rb} +14 -16
  39. data/spec/symbol_spec.rb +9 -0
  40. metadata +58 -23
  41. data/test/plucky/pagination/test_paginator.rb +0 -120
  42. data/test/plucky/test_criteria_hash.rb +0 -359
  43. data/test/plucky/test_options_hash.rb +0 -302
  44. data/test/plucky/test_query.rb +0 -843
  45. data/test/test_plucky.rb +0 -48
  46. data/test/test_symbol.rb +0 -11
@@ -0,0 +1,64 @@
1
+ require 'helper'
2
+
3
+ describe Plucky::OptionsHash do
4
+ describe "#initialize_copy" do
5
+ before do
6
+ @original = described_class.new(:fields => {:name => true}, :sort => :name, :limit => 10)
7
+ @cloned = @original.clone
8
+ end
9
+
10
+ it "duplicates source hash" do
11
+ @cloned.source.should_not equal(@original.source)
12
+ end
13
+
14
+ it "clones duplicable? values" do
15
+ @cloned[:fields].should_not equal(@original[:fields])
16
+ @cloned[:sort].should_not equal(@original[:sort])
17
+ end
18
+ end
19
+
20
+ describe "#fields?" do
21
+ it "returns true if fields have been selected" do
22
+ described_class.new(:fields => :name).fields?.should be(true)
23
+ end
24
+
25
+ it "returns false if no fields have been selected" do
26
+ described_class.new.fields?.should be(false)
27
+ end
28
+ end
29
+
30
+ describe "#merge" do
31
+ before do
32
+ @o1 = described_class.new(:skip => 5, :sort => :name)
33
+ @o2 = described_class.new(:limit => 10, :skip => 15)
34
+ @merged = @o1.merge(@o2)
35
+ end
36
+
37
+ it "overrides options in first with options in second" do
38
+ @merged.should == described_class.new(:limit => 10, :skip => 15, :sort => :name)
39
+ end
40
+
41
+ it "returns new instance and not change either of the merged" do
42
+ @o1[:skip].should == 5
43
+ @o2[:sort].should be_nil
44
+ @merged.should_not equal(@o1)
45
+ @merged.should_not equal(@o2)
46
+ end
47
+ end
48
+
49
+ describe "#merge!" do
50
+ before do
51
+ @o1 = described_class.new(:skip => 5, :sort => :name)
52
+ @o2 = described_class.new(:limit => 10, :skip => 15)
53
+ @merged = @o1.merge!(@o2)
54
+ end
55
+
56
+ it "overrides options in first with options in second" do
57
+ @merged.should == described_class.new(:limit => 10, :skip => 15, :sort => :name)
58
+ end
59
+
60
+ it "just updates the first" do
61
+ @merged.should equal(@o1)
62
+ end
63
+ end
64
+ end
@@ -1,34 +1,32 @@
1
1
  require 'helper'
2
2
 
3
- class PaginatorTest < Test::Unit::TestCase
4
- include Plucky::Pagination
5
-
3
+ describe Plucky::Pagination::Decorator do
6
4
  context "Object decorated with Decorator with paginator set" do
7
- setup do
5
+ before do
8
6
  @object = [1, 2, 3, 4]
9
7
  @object_id = @object.object_id
10
- @paginator = Paginator.new(20, 2, 10)
11
- @object.extend(Decorator)
8
+ @paginator = Plucky::Pagination::Paginator.new(20, 2, 10)
9
+ @object.extend(described_class)
12
10
  @object.paginator(@paginator)
13
11
  end
14
12
  subject { @object }
15
13
 
16
- should "be able to get paginator" do
14
+ it "knows paginator" do
17
15
  subject.paginator.should == @paginator
18
16
  end
19
17
 
20
18
  [:total_entries, :current_page, :per_page, :total_pages, :out_of_bounds?,
21
19
  :previous_page, :next_page, :skip, :limit, :offset].each do |method|
22
- should "delegate #{method} to paginator" do
20
+ it "delegates #{method} to paginator" do
23
21
  subject.send(method).should == @paginator.send(method)
24
22
  end
25
23
  end
26
24
 
27
- should "not interfere with other methods on the object" do
25
+ it "does not interfere with other methods on the object" do
28
26
  @object.object_id.should == @object_id
29
27
  @object.should == [1, 2, 3, 4]
30
28
  @object.size.should == 4
31
29
  @object.select { |o| o > 2 }.should == [3, 4]
32
30
  end
33
31
  end
34
- end
32
+ end
@@ -0,0 +1,118 @@
1
+ require 'helper'
2
+
3
+ describe Plucky::Pagination::Paginator do
4
+ describe "#initialize" do
5
+ context "with total and page" do
6
+ before { @paginator = described_class.new(20, 2) }
7
+ subject { @paginator }
8
+
9
+ it "sets total" do
10
+ subject.total_entries.should == 20
11
+ end
12
+
13
+ it "sets page" do
14
+ subject.current_page.should == 2
15
+ end
16
+
17
+ it "defaults per_page to 25" do
18
+ subject.per_page.should == 25
19
+ end
20
+ end
21
+
22
+ context "with total, page and per_page" do
23
+ before { @paginator = described_class.new(20, 2, 10) }
24
+ subject { @paginator }
25
+
26
+ it "sets total" do
27
+ subject.total_entries.should == 20
28
+ end
29
+
30
+ it "sets page" do
31
+ subject.current_page.should == 2
32
+ end
33
+
34
+ it "sets per_page" do
35
+ subject.per_page.should == 10
36
+ end
37
+ end
38
+
39
+ context "with string values for total, page and per_page" do
40
+ before { @paginator = described_class.new('20', '2', '10') }
41
+ subject { @paginator }
42
+
43
+ it "sets total" do
44
+ subject.total_entries.should == 20
45
+ end
46
+
47
+ it "sets page" do
48
+ subject.current_page.should == 2
49
+ end
50
+
51
+ it "sets per_page" do
52
+ subject.per_page.should == 10
53
+ end
54
+ end
55
+
56
+ context "with page less than 1" do
57
+ before { @paginator = described_class.new(20, -2, 10) }
58
+ subject { @paginator }
59
+
60
+ it "sets page to 1" do
61
+ subject.current_page.should == 1
62
+ end
63
+ end
64
+ end
65
+
66
+ it "aliases limit to per_page" do
67
+ described_class.new(30, 2, 30).limit.should == 30
68
+ end
69
+
70
+ it "knows total number of pages" do
71
+ described_class.new(43, 2, 7).total_pages.should == 7
72
+ described_class.new(40, 2, 10).total_pages.should == 4
73
+ end
74
+
75
+ describe "#out_of_bounds?" do
76
+ it "returns true if current_page is greater than total_pages" do
77
+ described_class.new(2, 3, 1).should be_out_of_bounds
78
+ end
79
+
80
+ it "returns false if current page is less than total_pages" do
81
+ described_class.new(2, 1, 1).should_not be_out_of_bounds
82
+ end
83
+
84
+ it "returns false if current page equals total_pages" do
85
+ described_class.new(2, 2, 1).should_not be_out_of_bounds
86
+ end
87
+ end
88
+
89
+ describe "#previous_page" do
90
+ it "returns nil if there is no page less than current" do
91
+ described_class.new(2, 1, 1).previous_page.should be_nil
92
+ end
93
+
94
+ it "returns number less than current page if there is one" do
95
+ described_class.new(2, 2, 1).previous_page.should == 1
96
+ end
97
+ end
98
+
99
+ describe "#next_page" do
100
+ it "returns nil if no page greater than current page" do
101
+ described_class.new(2, 2, 1).next_page.should be_nil
102
+ end
103
+
104
+ it "returns number greater than current page if there is one" do
105
+ described_class.new(2, 1, 1).next_page.should == 2
106
+ end
107
+ end
108
+
109
+ describe "#skip" do
110
+ it "works" do
111
+ described_class.new(30, 3, 10).skip.should == 20
112
+ end
113
+
114
+ it "returns aliased to offset for will paginate" do
115
+ described_class.new(30, 3, 10).offset.should == 20
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,839 @@
1
+ require 'helper'
2
+
3
+ describe Plucky::Query do
4
+ before do
5
+ @chris = BSON::OrderedHash['_id', 'chris', 'age', 26, 'name', 'Chris']
6
+ @steve = BSON::OrderedHash['_id', 'steve', 'age', 29, 'name', 'Steve']
7
+ @john = BSON::OrderedHash['_id', 'john', 'age', 28, 'name', 'John']
8
+ @collection = DB['users']
9
+ @collection.insert(@chris)
10
+ @collection.insert(@steve)
11
+ @collection.insert(@john)
12
+ end
13
+
14
+ context "#initialize" do
15
+ before { @query = described_class.new(@collection) }
16
+ subject { @query }
17
+
18
+ it "defaults options to options hash" do
19
+ @query.options.should be_instance_of(Plucky::OptionsHash)
20
+ end
21
+
22
+ it "defaults criteria to criteria hash" do
23
+ @query.criteria.should be_instance_of(Plucky::CriteriaHash)
24
+ end
25
+ end
26
+
27
+ context "#initialize_copy" do
28
+ before do
29
+ @original = described_class.new(@collection)
30
+ @cloned = @original.clone
31
+ end
32
+
33
+ it "duplicates options" do
34
+ @cloned.options.should_not equal(@original.options)
35
+ end
36
+
37
+ it "duplicates criteria" do
38
+ @cloned.criteria.should_not equal(@original.criteria)
39
+ end
40
+ end
41
+
42
+ context "#[]=" do
43
+ before { @query = described_class.new(@collection) }
44
+ subject { @query }
45
+
46
+ it "sets key on options for option" do
47
+ subject[:skip] = 1
48
+ subject[:skip].should == 1
49
+ end
50
+
51
+ it "sets key on criteria for criteria" do
52
+ subject[:foo] = 'bar'
53
+ subject[:foo].should == 'bar'
54
+ end
55
+ end
56
+
57
+ context "#find_each" do
58
+ it "returns a cursor" do
59
+ cursor = described_class.new(@collection).find_each
60
+ cursor.should be_instance_of(Mongo::Cursor)
61
+ end
62
+
63
+ it "works with and normalize criteria" do
64
+ cursor = described_class.new(@collection).find_each(:id.in => ['john'])
65
+ cursor.to_a.should == [@john]
66
+ end
67
+
68
+ it "works with and normalize options" do
69
+ cursor = described_class.new(@collection).find_each(:order => :name.asc)
70
+ cursor.to_a.should == [@chris, @john, @steve]
71
+ end
72
+
73
+ it "yields elements to a block if given" do
74
+ yielded_elements = Set.new
75
+ described_class.new(@collection).find_each { |doc| yielded_elements << doc }
76
+ yielded_elements.should == [@chris, @john, @steve].to_set
77
+ end
78
+
79
+ it "is Ruby-like and returns a reset cursor if a block is given" do
80
+ cursor = described_class.new(@collection).find_each {}
81
+ cursor.should be_instance_of(Mongo::Cursor)
82
+ cursor.next.should be_instance_of(BSON::OrderedHash)
83
+ end
84
+ end
85
+
86
+ context "#find_one" do
87
+ it "works with and normalize criteria" do
88
+ described_class.new(@collection).find_one(:id.in => ['john']).should == @john
89
+ end
90
+
91
+ it "works with and normalize options" do
92
+ described_class.new(@collection).find_one(:order => :age.desc).should == @steve
93
+ end
94
+ end
95
+
96
+ context "#find" do
97
+ before do
98
+ @query = described_class.new(@collection)
99
+ end
100
+ subject { @query }
101
+
102
+ it "works with single id" do
103
+ @query.find('chris').should == @chris
104
+ end
105
+
106
+ it "works with multiple ids" do
107
+ @query.find('chris', 'john').should == [@chris, @john]
108
+ end
109
+
110
+ it "works with array of one id" do
111
+ @query.find(['chris']).should == [@chris]
112
+ end
113
+
114
+ it "works with array of ids" do
115
+ @query.find(['chris', 'john']).should == [@chris, @john]
116
+ end
117
+
118
+ it "ignores those not found" do
119
+ @query.find('john', 'frank').should == [@john]
120
+ end
121
+
122
+ it "returns nil for nil" do
123
+ @query.find(nil).should be_nil
124
+ end
125
+
126
+ it "returns nil for *nil" do
127
+ @query.find(*nil).should be_nil
128
+ end
129
+
130
+ it "normalizes if using object id" do
131
+ id = @collection.insert(:name => 'Frank')
132
+ @query.object_ids([:_id])
133
+ doc = @query.find(id.to_s)
134
+ doc['name'].should == 'Frank'
135
+ end
136
+ end
137
+
138
+ context "#per_page" do
139
+ it "defaults to 25" do
140
+ described_class.new(@collection).per_page.should == 25
141
+ end
142
+
143
+ it "is changeable and chainable" do
144
+ query = described_class.new(@collection)
145
+ query.per_page(10).per_page.should == 10
146
+ end
147
+ end
148
+
149
+ context "#paginate" do
150
+ before do
151
+ @query = described_class.new(@collection).sort(:age).per_page(1)
152
+ end
153
+ subject { @query }
154
+
155
+ it "defaults to page 1" do
156
+ subject.paginate.should == [@chris]
157
+ end
158
+
159
+ it "works with other pages" do
160
+ subject.paginate(:page => 2).should == [@john]
161
+ subject.paginate(:page => 3).should == [@steve]
162
+ end
163
+
164
+ it "works with string page number" do
165
+ subject.paginate(:page => '2').should == [@john]
166
+ end
167
+
168
+ it "allows changing per_page" do
169
+ subject.paginate(:per_page => 2).should == [@chris, @john]
170
+ end
171
+
172
+ it "decorates return value" do
173
+ docs = subject.paginate
174
+ docs.should respond_to(:paginator)
175
+ docs.should respond_to(:total_entries)
176
+ end
177
+
178
+ it "does not modify the original query" do
179
+ subject.paginate(:name => 'John')
180
+ subject[:page].should be_nil
181
+ subject[:per_page].should be_nil
182
+ subject[:name].should be_nil
183
+ end
184
+
185
+ context "with options" do
186
+ before do
187
+ @result = @query.sort(:age).paginate(:age.gt => 27, :per_page => 10)
188
+ end
189
+ subject { @result }
190
+
191
+ it "only returns matching" do
192
+ subject.should == [@john, @steve]
193
+ end
194
+
195
+ it "correctly counts matching" do
196
+ subject.total_entries.should == 2
197
+ end
198
+ end
199
+ end
200
+
201
+ context "#all" do
202
+ it "works with no arguments" do
203
+ docs = described_class.new(@collection).all
204
+ docs.size.should == 3
205
+ docs.should include(@john)
206
+ docs.should include(@steve)
207
+ docs.should include(@chris)
208
+ end
209
+
210
+ it "works with and normalize criteria" do
211
+ docs = described_class.new(@collection).all(:id.in => ['steve'])
212
+ docs.should == [@steve]
213
+ end
214
+
215
+ it "works with and normalize options" do
216
+ docs = described_class.new(@collection).all(:order => :name.asc)
217
+ docs.should == [@chris, @john, @steve]
218
+ end
219
+
220
+ it "does not modify original query object" do
221
+ query = described_class.new(@collection)
222
+ query.all(:name => 'Steve')
223
+ query[:name].should be_nil
224
+ end
225
+ end
226
+
227
+ context "#first" do
228
+ it "works with and normalize criteria" do
229
+ described_class.new(@collection).first(:age.lt => 29).should == @chris
230
+ end
231
+
232
+ it "works with and normalize options" do
233
+ described_class.new(@collection).first(:age.lte => 29, :order => :name.desc).should == @steve
234
+ end
235
+
236
+ it "does not modify original query object" do
237
+ query = described_class.new(@collection)
238
+ query.first(:name => 'Steve')
239
+ query[:name].should be_nil
240
+ end
241
+ end
242
+
243
+ context "#last" do
244
+ it "works with and normalize criteria" do
245
+ described_class.new(@collection).last(:age.lte => 29, :order => :name.asc).should == @steve
246
+ end
247
+
248
+ it "works with and normalize options" do
249
+ described_class.new(@collection).last(:age.lte => 26, :order => :name.desc).should == @chris
250
+ end
251
+
252
+ it "does not modify original query object" do
253
+ query = described_class.new(@collection)
254
+ query.last(:name => 'Steve')
255
+ query[:name].should be_nil
256
+ end
257
+ end
258
+
259
+ context "#count" do
260
+ it "works with no arguments" do
261
+ described_class.new(@collection).count.should == 3
262
+ end
263
+
264
+ it "works with and normalize criteria" do
265
+ described_class.new(@collection).count(:age.lte => 28).should == 2
266
+ end
267
+
268
+ it "does not modify original query object" do
269
+ query = described_class.new(@collection)
270
+ query.count(:name => 'Steve')
271
+ query[:name].should be_nil
272
+ end
273
+ end
274
+
275
+ context "#size" do
276
+ it "works just like count without options" do
277
+ described_class.new(@collection).size.should == 3
278
+ end
279
+ end
280
+
281
+ context "#distinct" do
282
+ before do
283
+ # same age as John
284
+ @mark = BSON::OrderedHash['_id', 'mark', 'age', 28, 'name', 'Mark']
285
+ @collection.insert(@mark)
286
+ end
287
+
288
+ it "works with just a key" do
289
+ described_class.new(@collection).distinct(:age).sort.should == [26, 28, 29]
290
+ end
291
+
292
+ it "works with criteria" do
293
+ described_class.new(@collection).distinct(:age, :age.gt => 26).sort.should == [28, 29]
294
+ end
295
+
296
+ it "does not modify the original query object" do
297
+ query = described_class.new(@collection)
298
+ query.distinct(:age, :name => 'Mark').should == [28]
299
+ query[:name].should be_nil
300
+ end
301
+ end
302
+
303
+ context "#remove" do
304
+ it "works with no arguments" do
305
+ lambda { described_class.new(@collection).remove }.should change { @collection.count }.by(-3)
306
+ end
307
+
308
+ it "works with and normalize criteria" do
309
+ lambda { described_class.new(@collection).remove(:age.lte => 28) }.should change { @collection.count }
310
+ end
311
+
312
+ it "works with options" do
313
+ lambda { described_class.new(@collection).remove({:age.lte => 28}, :w => 1) }.should change { @collection.count }
314
+ end
315
+
316
+ it "does not modify original query object" do
317
+ query = described_class.new(@collection)
318
+ query.remove(:name => 'Steve')
319
+ query[:name].should be_nil
320
+ end
321
+ end
322
+
323
+ context "#update" do
324
+ before do
325
+ @query = described_class.new(@collection).where('_id' => 'john')
326
+ end
327
+
328
+ it "works with document" do
329
+ @query.update('$set' => {'age' => 29})
330
+ doc = @query.first('_id' => 'john')
331
+ doc['age'].should be(29)
332
+ end
333
+
334
+ it "works with document and driver options" do
335
+ @query.update({'$set' => {'age' => 30}}, :multi => true)
336
+ @query.each do |doc|
337
+ doc['age'].should be(30)
338
+ end
339
+ end
340
+ end
341
+
342
+ context "#[]" do
343
+ it "returns value if key in criteria (symbol)" do
344
+ described_class.new(@collection, :count => 1)[:count].should == 1
345
+ end
346
+
347
+ it "returns value if key in criteria (string)" do
348
+ described_class.new(@collection, :count => 1)['count'].should == 1
349
+ end
350
+
351
+ it "returns nil if key not in criteria" do
352
+ described_class.new(@collection)[:count].should be_nil
353
+ end
354
+ end
355
+
356
+ context "#[]=" do
357
+ before { @query = described_class.new(@collection) }
358
+
359
+ it "sets the value of the given criteria key" do
360
+ @query[:count] = 1
361
+ @query[:count].should == 1
362
+ end
363
+
364
+ it "overwrites value if key already exists" do
365
+ @query[:count] = 1
366
+ @query[:count] = 2
367
+ @query[:count].should == 2
368
+ end
369
+
370
+ it "normalizes value" do
371
+ now = Time.now
372
+ @query[:published_at] = now
373
+ @query[:published_at].should == now.utc
374
+ end
375
+ end
376
+
377
+ context "#fields" do
378
+ before { @query = described_class.new(@collection) }
379
+ subject { @query }
380
+
381
+ it "works" do
382
+ subject.fields(:name).first(:id => 'john').keys.should == ['_id', 'name']
383
+ end
384
+
385
+ it "returns new instance of query" do
386
+ new_query = subject.fields(:name)
387
+ new_query.should_not equal(subject)
388
+ subject[:fields].should be_nil
389
+ end
390
+
391
+ it "works with hash" do
392
+ subject.fields(:name => 0).
393
+ first(:id => 'john').keys.sort.
394
+ should == ['_id', 'age']
395
+ end
396
+ end
397
+
398
+ context "#ignore" do
399
+ before { @query = described_class.new(@collection) }
400
+ subject { @query }
401
+
402
+ it "includes a list of keys to ignore" do
403
+ new_query = subject.ignore(:name).first(:id => 'john')
404
+ new_query.keys.should == ['_id', 'age']
405
+ end
406
+ end
407
+
408
+ context "#only" do
409
+ before { @query = described_class.new(@collection) }
410
+ subject { @query }
411
+
412
+ it "includes a list of keys with others excluded" do
413
+ new_query = subject.only(:name).first(:id => 'john')
414
+ new_query.keys.should == ['_id', 'name']
415
+ end
416
+
417
+ end
418
+
419
+ context "#skip" do
420
+ before { @query = described_class.new(@collection) }
421
+ subject { @query }
422
+
423
+ it "works" do
424
+ subject.skip(2).all(:order => :age).should == [@steve]
425
+ end
426
+
427
+ it "sets skip option" do
428
+ subject.skip(5).options[:skip].should == 5
429
+ end
430
+
431
+ it "overrides existing skip" do
432
+ subject.skip(5).skip(10).options[:skip].should == 10
433
+ end
434
+
435
+ it "returns nil for nil" do
436
+ subject.skip.options[:skip].should be_nil
437
+ end
438
+
439
+ it "returns new instance of query" do
440
+ new_query = subject.skip(2)
441
+ new_query.should_not equal(subject)
442
+ subject[:skip].should be_nil
443
+ end
444
+
445
+ it "aliases to offset" do
446
+ subject.offset(5).options[:skip].should == 5
447
+ end
448
+ end
449
+
450
+ context "#limit" do
451
+ before { @query = described_class.new(@collection) }
452
+ subject { @query }
453
+
454
+ it "works" do
455
+ subject.limit(2).all(:order => :age).should == [@chris, @john]
456
+ end
457
+
458
+ it "sets limit option" do
459
+ subject.limit(5).options[:limit].should == 5
460
+ end
461
+
462
+ it "overwrites existing limit" do
463
+ subject.limit(5).limit(15).options[:limit].should == 15
464
+ end
465
+
466
+ it "returns new instance of query" do
467
+ new_query = subject.limit(2)
468
+ new_query.should_not equal(subject)
469
+ subject[:limit].should be_nil
470
+ end
471
+ end
472
+
473
+ context "#sort" do
474
+ before { @query = described_class.new(@collection) }
475
+ subject { @query }
476
+
477
+ it "works" do
478
+ subject.sort(:age).all.should == [@chris, @john, @steve]
479
+ subject.sort(:age.desc).all.should == [@steve, @john, @chris]
480
+ end
481
+
482
+ it "works with symbol operators" do
483
+ subject.sort(:foo.asc, :bar.desc).options[:sort].should == [['foo', 1], ['bar', -1]]
484
+ end
485
+
486
+ it "works with string" do
487
+ subject.sort('foo, bar desc').options[:sort].should == [['foo', 1], ['bar', -1]]
488
+ end
489
+
490
+ it "works with just a symbol" do
491
+ subject.sort(:foo).options[:sort].should == [['foo', 1]]
492
+ end
493
+
494
+ it "works with symbol descending" do
495
+ subject.sort(:foo.desc).options[:sort].should == [['foo', -1]]
496
+ end
497
+
498
+ it "works with multiple symbols" do
499
+ subject.sort(:foo, :bar).options[:sort].should == [['foo', 1], ['bar', 1]]
500
+ end
501
+
502
+ it "returns new instance of query" do
503
+ new_query = subject.sort(:name)
504
+ new_query.should_not equal(subject)
505
+ subject[:sort].should be_nil
506
+ end
507
+
508
+ it "is aliased to order" do
509
+ subject.order(:foo).options[:sort].should == [['foo', 1]]
510
+ subject.order(:foo, :bar).options[:sort].should == [['foo', 1], ['bar', 1]]
511
+ end
512
+ end
513
+
514
+ context "#reverse" do
515
+ before { @query = described_class.new(@collection) }
516
+ subject { @query }
517
+
518
+ it "works" do
519
+ subject.sort(:age).reverse.all.should == [@steve, @john, @chris]
520
+ end
521
+
522
+ it "does not error if no sort provided" do
523
+ expect {
524
+ subject.reverse
525
+ }.to_not raise_error
526
+ end
527
+
528
+ it "reverses the sort order" do
529
+ subject.sort('foo asc, bar desc').
530
+ reverse.options[:sort].should == [['foo', -1], ['bar', 1]]
531
+ end
532
+
533
+ it "returns new instance of query" do
534
+ sorted_query = subject.sort(:name)
535
+ new_query = sorted_query.reverse
536
+ new_query.should_not equal(sorted_query)
537
+ sorted_query[:sort].should == [['name', 1]]
538
+ end
539
+ end
540
+
541
+ context "#amend" do
542
+ it "normalizes and update options" do
543
+ described_class.new(@collection).amend(:order => :age.desc).options[:sort].should == [['age', -1]]
544
+ end
545
+
546
+ it "works with simple stuff" do
547
+ described_class.new(@collection).
548
+ amend(:foo => 'bar').
549
+ amend(:baz => 'wick').
550
+ criteria.source.should eq(:foo => 'bar', :baz => 'wick')
551
+ end
552
+ end
553
+
554
+ context "#where" do
555
+ before { @query = described_class.new(@collection) }
556
+ subject { @query }
557
+
558
+ it "works" do
559
+ subject.where(:age.lt => 29).where(:name => 'Chris').all.should == [@chris]
560
+ end
561
+
562
+ it "works with literal regexp" do
563
+ subject.where(:name => /^c/i).all.should == [@chris]
564
+ end
565
+
566
+ it "updates criteria" do
567
+ subject.
568
+ where(:moo => 'cow').
569
+ where(:foo => 'bar').
570
+ criteria.source.should eq(:foo => 'bar', :moo => 'cow')
571
+ end
572
+
573
+ it "gets normalized" do
574
+ subject.
575
+ where(:moo => 'cow').
576
+ where(:foo.in => ['bar']).
577
+ criteria.source.should eq(:moo => 'cow', :foo => {:$in => ['bar']})
578
+ end
579
+
580
+ it "normalizes merged criteria" do
581
+ subject.
582
+ where(:foo => 'bar').
583
+ where(:foo => 'baz').
584
+ criteria.source.should eq(:foo => {:$in => %w[bar baz]})
585
+ end
586
+
587
+ it "returns new instance of query" do
588
+ new_query = subject.where(:name => 'John')
589
+ new_query.should_not equal(subject)
590
+ subject[:name].should be_nil
591
+ end
592
+ end
593
+
594
+ context "#filter" do
595
+ before { @query = described_class.new(@collection) }
596
+ subject { @query }
597
+
598
+ it "works the same as where" do
599
+ subject.filter(:age.lt => 29).filter(:name => 'Chris').all.should == [@chris]
600
+ end
601
+ end
602
+
603
+ context "#empty?" do
604
+ it "returns true if empty" do
605
+ @collection.remove
606
+ described_class.new(@collection).should be_empty
607
+ end
608
+
609
+ it "returns false if not empty" do
610
+ described_class.new(@collection).should_not be_empty
611
+ end
612
+ end
613
+
614
+ context "#exists?" do
615
+ it "returns true if found" do
616
+ described_class.new(@collection).exists?(:name => 'John').should be(true)
617
+ end
618
+
619
+ it "returns false if not found" do
620
+ described_class.new(@collection).exists?(:name => 'Billy Bob').should be(false)
621
+ end
622
+ end
623
+
624
+ context "#exist?" do
625
+ it "returns true if found" do
626
+ described_class.new(@collection).exist?(:name => 'John').should be(true)
627
+ end
628
+
629
+ it "returns false if not found" do
630
+ described_class.new(@collection).exist?(:name => 'Billy Bob').should be(false)
631
+ end
632
+ end
633
+
634
+ context "#include?" do
635
+ it "returns true if included" do
636
+ described_class.new(@collection).include?(@john).should be(true)
637
+ end
638
+
639
+ it "returns false if not included" do
640
+ described_class.new(@collection).include?(['_id', 'frankyboy']).should be(false)
641
+ end
642
+ end
643
+
644
+ context "#to_a" do
645
+ it "returns all documents the query matches" do
646
+ described_class.new(@collection).sort(:name).to_a.
647
+ should == [@chris, @john, @steve]
648
+
649
+ described_class.new(@collection).where(:name => 'John').sort(:name).to_a.
650
+ should == [@john]
651
+ end
652
+ end
653
+
654
+ context "#each" do
655
+ it "iterates through matching documents" do
656
+ docs = []
657
+ described_class.new(@collection).sort(:name).each do |doc|
658
+ docs << doc
659
+ end
660
+ docs.should == [@chris, @john, @steve]
661
+ end
662
+
663
+ it "returns a working enumerator" do
664
+ query = described_class.new(@collection)
665
+ query.each.methods.map(&:to_sym).include?(:group_by).should be(true)
666
+ query.each.next.should be_instance_of(BSON::OrderedHash)
667
+ end
668
+
669
+ it "uses #find_each" do
670
+ query = described_class.new(@collection)
671
+ query.should_receive(:find_each)
672
+ query.each
673
+ end
674
+ end
675
+
676
+ context "enumerables" do
677
+ it "works" do
678
+ query = described_class.new(@collection).sort(:name)
679
+ query.map { |doc| doc['name'] }.should == %w(Chris John Steve)
680
+ query.collect { |doc| doc['name'] }.should == %w(Chris John Steve)
681
+ query.detect { |doc| doc['name'] == 'John' }.should == @john
682
+ query.min { |a, b| a['age'] <=> b['age'] }.should == @chris
683
+ end
684
+ end
685
+
686
+ context "#object_ids" do
687
+ before { @query = described_class.new(@collection) }
688
+ subject { @query }
689
+
690
+ it "sets criteria's object_ids" do
691
+ subject.criteria.should_receive(:object_ids=).with([:foo, :bar])
692
+ subject.object_ids(:foo, :bar)
693
+ end
694
+
695
+ it "returns current object ids if keys argument is empty" do
696
+ subject.object_ids(:foo, :bar)
697
+ subject.object_ids.should == [:foo, :bar]
698
+ end
699
+ end
700
+
701
+ context "#merge" do
702
+ it "overwrites options" do
703
+ query1 = described_class.new(@collection, :skip => 5, :limit => 5)
704
+ query2 = described_class.new(@collection, :skip => 10, :limit => 10)
705
+ new_query = query1.merge(query2)
706
+ new_query.options[:skip].should == 10
707
+ new_query.options[:limit].should == 10
708
+ end
709
+
710
+ it "merges criteria" do
711
+ query1 = described_class.new(@collection, :foo => 'bar')
712
+ query2 = described_class.new(@collection, :foo => 'baz', :fent => 'wick')
713
+ new_query = query1.merge(query2)
714
+ new_query.criteria[:fent].should == 'wick'
715
+ new_query.criteria[:foo].should == {:$in => %w[bar baz]}
716
+ end
717
+
718
+ it "does not affect either of the merged queries" do
719
+ query1 = described_class.new(@collection, :foo => 'bar', :limit => 5)
720
+ query2 = described_class.new(@collection, :foo => 'baz', :limit => 10)
721
+ new_query = query1.merge(query2)
722
+ query1[:foo].should == 'bar'
723
+ query1[:limit].should == 5
724
+ query2[:foo].should == 'baz'
725
+ query2[:limit].should == 10
726
+ end
727
+ end
728
+
729
+ context "Criteria/option auto-detection" do
730
+ it "knows :conditions are criteria" do
731
+ query = described_class.new(@collection, :conditions => {:foo => 'bar'})
732
+ query.criteria.source.should eq(:foo => 'bar')
733
+ query.options.keys.should_not include(:conditions)
734
+ end
735
+
736
+ {
737
+ :fields => ['foo'],
738
+ :sort => [['foo', 1]],
739
+ :hint => '',
740
+ :skip => 0,
741
+ :limit => 0,
742
+ :batch_size => 0,
743
+ :timeout => 0,
744
+ }.each do |option, value|
745
+ it "knows #{option} is an option" do
746
+ query = described_class.new(@collection, option => value)
747
+ query.options[option].should == value
748
+ query.criteria.keys.should_not include(option)
749
+ end
750
+ end
751
+
752
+ it "knows select is an option and remove it from options" do
753
+ query = described_class.new(@collection, :select => 'foo')
754
+ query.options[:fields].should == ['foo']
755
+ query.criteria.keys.should_not include(:select)
756
+ query.options.keys.should_not include(:select)
757
+ end
758
+
759
+ it "knows order is an option and remove it from options" do
760
+ query = described_class.new(@collection, :order => 'foo')
761
+ query.options[:sort].should == [['foo', 1]]
762
+ query.criteria.keys.should_not include(:order)
763
+ query.options.keys.should_not include(:order)
764
+ end
765
+
766
+ it "knows offset is an option and remove it from options" do
767
+ query = described_class.new(@collection, :offset => 0)
768
+ query.options[:skip].should == 0
769
+ query.criteria.keys.should_not include(:offset)
770
+ query.options.keys.should_not include(:offset)
771
+ end
772
+
773
+ it "works with full range of things" do
774
+ query = described_class.new(@collection, {
775
+ :foo => 'bar',
776
+ :baz => true,
777
+ :sort => [['foo', 1]],
778
+ :fields => ['foo', 'baz'],
779
+ :limit => 10,
780
+ :skip => 10,
781
+ })
782
+ query.criteria.source.should eq(:foo => 'bar', :baz => true)
783
+ query.options.source.should eq({
784
+ :sort => [['foo', 1]],
785
+ :fields => ['foo', 'baz'],
786
+ :limit => 10,
787
+ :skip => 10,
788
+ })
789
+ end
790
+ end
791
+
792
+ it "inspects pretty" do
793
+ inspect = described_class.new(@collection, :baz => 'wick', :foo => 'bar').inspect
794
+ inspect.should == '#<Plucky::Query baz: "wick", foo: "bar">'
795
+ end
796
+
797
+ it "delegates simple? to criteria" do
798
+ query = described_class.new(@collection)
799
+ query.criteria.should_receive(:simple?)
800
+ query.simple?
801
+ end
802
+
803
+ it "delegates fields? to options" do
804
+ query = described_class.new(@collection)
805
+ query.options.should_receive(:fields?)
806
+ query.fields?
807
+ end
808
+
809
+ context "#explain" do
810
+ before { @query = described_class.new(@collection) }
811
+ subject { @query }
812
+
813
+ it "works" do
814
+ explain = subject.where(:age.lt => 28).explain
815
+ explain['cursor'].should == 'BasicCursor'
816
+ explain['nscanned'].should == 3
817
+ end
818
+ end
819
+
820
+ context "Transforming documents" do
821
+ before do
822
+ transformer = lambda { |doc| @user_class.new(doc['_id'], doc['name'], doc['age']) }
823
+ @user_class = Struct.new(:id, :name, :age)
824
+ @query = described_class.new(@collection, :transformer => transformer)
825
+ end
826
+
827
+ it "works with find_one" do
828
+ result = @query.find_one('_id' => 'john')
829
+ result.should be_instance_of(@user_class)
830
+ end
831
+
832
+ it "works with find_each" do
833
+ results = @query.find_each
834
+ results.each do |result|
835
+ result.should be_instance_of(@user_class)
836
+ end
837
+ end
838
+ end
839
+ end