plucky 0.5.2 → 0.6.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.
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