searchlight 3.1.1 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,27 @@
1
+ module Searchlight::Options
2
+
3
+ def self.empty?(value)
4
+ return true if value.nil?
5
+ return true if value.respond_to?(:empty?) && value.empty?
6
+ return true if value.is_a?(String) && /\A[[:space:]]*\z/ === value
7
+ false
8
+ end
9
+
10
+ def self.checked?(value)
11
+ !(['0', 'false', ''].include?(value.to_s.strip))
12
+ end
13
+
14
+ def self.excluding_empties(input)
15
+ output = input.dup
16
+ output.each do |key, value|
17
+ if value.is_a?(Array)
18
+ output[key] = value.reject { |v| empty?(v) }
19
+ end
20
+ if value.is_a?(Hash)
21
+ output[key] = value.reject { |k, v| empty?(v) }
22
+ end
23
+ end
24
+ output.reject { |key, value| empty?(value) }
25
+ end
26
+
27
+ end
@@ -1,117 +1,69 @@
1
- module Searchlight
2
- class Search
3
- extend DSL
1
+ require_relative "options"
4
2
 
5
- def self.search_target
6
- return @search_target if defined?(@search_target)
7
- return superclass.search_target if superclass.respond_to?(:search_target) && superclass != Searchlight::Search
8
- guess_search_class!
9
- end
10
-
11
- def initialize(options = {})
12
- filter_and_mass_assign(options)
13
- end
14
-
15
- def search
16
- @search ||= begin
17
- target = self.class.search_target
18
- if callable?(target)
19
- # for delayed scope evaluation
20
- target.call
21
- else
22
- target
23
- end
24
- end
25
- end
26
-
27
- def results
28
- @results ||= run
29
- end
30
-
31
- def options
32
- search_attributes.reduce({}) { |hash, option_name|
33
- option_val = send(option_name)
34
- hash.tap { |hash| hash[option_name.to_sym] = option_val unless is_blank?(option_val) }
35
- }
36
- end
3
+ class Searchlight::Search
37
4
 
38
- protected
5
+ SEARCH_METHOD_PATTERN = /\Asearch_(?<option>.*)/
39
6
 
40
- attr_writer :search
41
-
42
- private
43
-
44
- def search_attributes
45
- public_methods.map(&:to_s).select { |m| m.start_with?('search_') }.map { |m| m.sub(/\Asearch_/, '') }
46
- end
47
-
48
- def self.guess_search_class!
49
- if self.name.end_with?('Search')
50
- @search_target = name.sub(/Search\z/, '').split('::').inject(Kernel, &:const_get)
51
- else
52
- raise MissingSearchTarget, "No search target provided via `search_on` and Searchlight can't guess one."
53
- end
54
- rescue NameError => e
55
- raise e unless /uninitialized constant/.match(e.message)
56
- raise MissingSearchTarget, "No search target provided via `search_on` and Searchlight's guess was wrong. Error: #{e.message}"
57
- end
58
-
59
- def self.search_target=(value)
60
- @search_target = value
61
- end
7
+ attr_accessor :query
8
+ attr_reader :raw_options
62
9
 
63
- def filter_and_mass_assign(provided_options = {})
64
- options = (provided_options || {}).reject { |key, value| is_blank?(value) }
65
- begin
66
- options.each { |key, value| public_send("#{key}=", value) } if options && options.any?
67
- rescue NoMethodError => e
68
- raise UndefinedOption.new(e.name, self)
10
+ def self.method_added(method_name)
11
+ method_name.to_s.match(SEARCH_METHOD_PATTERN) do |match|
12
+ option_name = match.captures.fetch(0)
13
+ # accessor - eg, if method_name is #search_title, define #title
14
+ define_method(option_name) do
15
+ options[option_name]
69
16
  end
70
17
  end
18
+ end
71
19
 
72
- def run
73
- options.each do |option_name, value|
74
- new_search = public_send("search_#{option_name}")
75
- self.search = new_search unless new_search.nil?
76
- end
77
- search
78
- end
20
+ def initialize(raw_options = {})
21
+ @raw_options = raw_options
22
+ end
79
23
 
80
- # Note that false is not blank
81
- def is_blank?(value)
82
- return true if value.respond_to?(:all?) && value.all? { |v| is_blank?(v) }
83
- return true if value.respond_to?(:empty?) && value.empty?
84
- return true if value.nil? || value.to_s.strip == ''
85
- false
86
- end
24
+ def results
25
+ @results ||= run
26
+ end
87
27
 
88
- def callable?(target)
89
- # The obvious implementation would be 'respond_to?(:call)`, but
90
- # then we may use Sequel::Dataset#call by accident.
91
- target.is_a?(Proc)
92
- end
28
+ def options
29
+ Searchlight::Options.excluding_empties(raw_options)
30
+ end
93
31
 
94
- MissingSearchTarget = Class.new(Searchlight::Error)
32
+ def empty?(value)
33
+ Searchlight::Options.empty?(value)
34
+ end
95
35
 
96
- class UndefinedOption < Searchlight::Error
36
+ def checked?(value)
37
+ Searchlight::Options.checked?(value)
38
+ end
97
39
 
98
- attr_accessor :message
40
+ def explain
41
+ [
42
+ "Initialized with `raw_options`: #{raw_options.keys.inspect}",
43
+ "Of those, the non-blank ones are available as `options`: #{options.keys.inspect}",
44
+ "Of those, the following have corresponding `search_` methods: #{options_with_search_methods.keys}. These would be used to build the query.",
45
+ "Blank options are: #{(raw_options.keys - options.keys).inspect}",
46
+ "Non-blank options with no corresponding `search_` method are: #{options.keys - options_with_search_methods.keys}",
47
+ ].join("\n\n")
48
+ end
99
49
 
100
- def initialize(option_name, search)
101
- option_name = option_name.to_s.sub(/=\Z/, '')
102
- self.message = "#{search.class.name} doesn't search '#{option_name}' or have an accessor for that property."
103
- if option_name.start_with?('search_')
104
- method_maybe_intended = option_name.sub(/\Asearch_/, '')
105
- # Gee golly, I'm so helpful!
106
- self.message << " Did you just mean '#{method_maybe_intended}'?" if search.respond_to?("#{method_maybe_intended}=")
107
- end
50
+ def options_with_search_methods
51
+ {}.tap do |map|
52
+ options.each do |option_name, option_value|
53
+ method_name = "search_#{option_name}"
54
+ map[option_name] = method_name if respond_to?(method_name)
108
55
  end
56
+ end
57
+ end
109
58
 
110
- def to_s
111
- message
112
- end
59
+ private
113
60
 
61
+ def run
62
+ self.query = base_query
63
+ options_with_search_methods.each do |option, method_name|
64
+ self.query = public_send(method_name)
114
65
  end
115
-
66
+ query
116
67
  end
68
+
117
69
  end
@@ -1,3 +1,3 @@
1
1
  module Searchlight
2
- VERSION = "3.1.1"
2
+ VERSION = "4.0.0"
3
3
  end
data/searchlight.gemspec CHANGED
@@ -19,12 +19,10 @@ Gem::Specification.new do |spec|
19
19
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
20
  spec.require_paths = ["lib"]
21
21
 
22
- spec.add_dependency "named", "~> 1.0"
23
-
24
- spec.add_development_dependency "rspec", "~> 2.14"
22
+ spec.add_development_dependency "rspec", "~> 3.2"
25
23
  spec.add_development_dependency "bundler", "~> 1.3"
26
24
  spec.add_development_dependency "rake"
27
- spec.add_development_dependency "capybara", "~> 2.0"
25
+ spec.add_development_dependency "capybara", "~> 2.4"
28
26
 
29
27
  # To test integration with actionview and activerecord
30
28
  spec.add_development_dependency "actionview", "~> 4.1"
@@ -1,25 +1,28 @@
1
- require 'spec_helper'
1
+ require "spec_helper"
2
2
 
3
- describe 'Searchlight::Adapters::ActionView', type: :feature, adapter: true do
3
+ describe "Searchlight::Adapters::ActionView", type: :feature do
4
4
 
5
5
  let(:view) { ::ActionView::Base.new }
6
- let(:search) { AccountSearch.new(paid_amount: 15) }
6
+ let(:search) { BookSearch.new("title_like" => "Love Among the Chickens") }
7
7
 
8
8
  before :all do
9
9
  # Only required when running these tests
10
- require 'searchlight/adapters/action_view'
10
+ require "searchlight/adapters/action_view"
11
+ BookSearch.send(:include, Searchlight::Adapters::ActionView)
11
12
  end
12
13
 
13
14
  before :each do
14
- view.stub(:protect_against_forgery?).and_return(false)
15
+ allow(view).to receive(:protect_against_forgery?).and_return(false)
15
16
  end
16
17
 
17
18
  it "it can be used to build a form" do
18
19
  form = view.form_for(search, url: '#') do |f|
19
- f.text_field(:paid_amount)
20
+ f.text_field(:title_like)
20
21
  end
21
22
 
22
- expect(form).to have_selector("form input[name='account_search[paid_amount]'][value='15']")
23
+ expect(form).to have_selector(
24
+ "form input[name='book_search[title_like]'][value='Love Among the Chickens']"
25
+ )
23
26
  end
24
27
 
25
28
  it "tells the form that it is not persisted" do
@@ -0,0 +1,104 @@
1
+ require 'spec_helper'
2
+
3
+ describe Searchlight::Options do
4
+
5
+ let(:mod) { described_class }
6
+
7
+ describe "checked?" do
8
+
9
+ {
10
+ true => true,
11
+ "true" => true,
12
+ "yeppers" => true,
13
+ 1 => true,
14
+ "1" => true,
15
+ 15 => true,
16
+ false => false,
17
+ nil => false,
18
+ "false" => false,
19
+ 0 => false,
20
+ "0" => false,
21
+ "" => false,
22
+ " " => false,
23
+ }.each do |input, output|
24
+
25
+ it "checked?(#{input.inspect}) is #{output.inspect}" do
26
+ expect(mod.checked?(input)).to eq(output)
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+
33
+ describe "empty?" do
34
+ [ nil, "", " ", " \n\t \r ", " ", "\u00a0", [], {} ].each do |blank_val|
35
+
36
+ it "empty?(#{blank_val.inspect}) is true" do
37
+ expect(mod.empty?(blank_val)).to eq(true)
38
+ end
39
+
40
+ end
41
+
42
+ [Object.new, true, false, 0, 1, "a", { nil => nil }].each do |present_val|
43
+
44
+ it "empty?(#{present_val.inspect}) is false" do
45
+ expect(mod.empty?(present_val)).to eq(false)
46
+ end
47
+
48
+ end
49
+
50
+ end
51
+
52
+ describe "excluding empties" do
53
+
54
+ it "removes empty values at the top level of the hash" do
55
+ expect(
56
+ mod.excluding_empties(
57
+ name: "Bob",
58
+ age: nil,
59
+ likes: ["pizza", "fish"],
60
+ dislikes: [],
61
+ elvish: false,
62
+ relations: {uncle: "Jimmy"},
63
+ eh: {},
64
+ )
65
+ ).to eq(
66
+ name: "Bob",
67
+ likes: ["pizza", "fish"],
68
+ relations: {uncle: "Jimmy"},
69
+ elvish: false,
70
+ )
71
+ end
72
+
73
+ # Because I don't expect such structures in form parameters.
74
+ # If I'm wrong, this can be changed to be recursive.
75
+ it "does not remove empty values from more deeply-nested elements" do
76
+ expect(
77
+ mod.excluding_empties(
78
+ tags: ["one", "two", "", nil, "three", ["a", "", nil, "b"], {a: ""}]
79
+ )
80
+ ).to eq(
81
+ tags: ["one", "two", "three", ["a", "", nil, "b"], {a: ""}]
82
+ )
83
+ end
84
+
85
+ it "does not modify the incoming hash" do
86
+ build_options_hash = proc {
87
+ {
88
+ name: "Bob",
89
+ age: nil,
90
+ likes: ["pizza", "fish", ""],
91
+ dislikes: [],
92
+ elvish: false,
93
+ relations: {uncle: "Jimmy", foo: nil},
94
+ eh: {},
95
+ }
96
+ }
97
+ examples = Array.new(2) { build_options_hash.call }
98
+ mod.excluding_empties(examples[0])
99
+ expect(examples[0]).to eq(examples[1])
100
+ end
101
+
102
+ end
103
+
104
+ end
@@ -2,399 +2,60 @@ require 'spec_helper'
2
2
 
3
3
  describe Searchlight::Search do
4
4
 
5
- let(:search_class) { Named::Class.new('ExampleSearch', described_class).tap {|klass|
6
- klass.searches *allowed_options
7
- allowed_options.each { |name| klass.send(:define_method, "search_#{name}") {} }
5
+ let(:raw_options) {
6
+ {
7
+ title_like: "Mere Christianity",
8
+ "author_name_like" => "Lew",
9
+ category_in: nil,
10
+ tags: ["", "fancy"],
11
+ book_thickness: "smallish",
12
+ parts_about_lolcats: "",
8
13
  }
9
14
  }
10
- let(:allowed_options) { Hash.new }
11
- let(:provided_options) { Hash.new }
12
- let(:search) { search_class.new(provided_options) }
15
+ let(:search) { BookSearch.new(raw_options) }
13
16
 
14
- describe "options" do
15
-
16
- context "when given valid options" do
17
-
18
- context "when the search class has no defaults" do
19
-
20
- describe "screening options" do
21
-
22
- let(:allowed_options) { [:name, :description, :categories, :nicknames] }
23
-
24
- context "when all options are usable" do
25
-
26
- let(:provided_options) { {name: 'Roy', description: 'Ornry', categories: %w[mammal moonshiner], nicknames: %w[Slim Bubba]} }
27
-
28
- it "adds them to the options accessor" do
29
- expect(search.options).to eq(provided_options)
30
- end
31
-
32
- end
33
-
34
- context "when some provided options are empty" do
35
-
36
- let(:provided_options) { {name: 'Roy', description: '', categories: ['', ''], nicknames: []} }
37
-
38
- it "does not add them to the options accessor" do
39
- expect(search.options).to eq(name: 'Roy')
40
- end
41
-
42
- end
43
-
44
- context "when an empty options hash is given" do
45
-
46
- let(:provided_options) { {} }
47
-
48
- it "has empty options" do
49
- expect(search.options).to eq({})
50
- end
51
-
52
- end
53
-
54
- context "when the options are explicitly nil" do
55
-
56
- let(:provided_options) { nil }
57
-
58
- it "has empty options" do
59
- expect(search.options).to eq({})
60
- end
61
-
62
- end
63
-
64
- context "when some options are do not map to search methods (eg, attr_accessor)" do
65
- let(:search_class) {
66
- Named::Class.new('ExampleSearch', described_class) do
67
- attr_accessor :krazy_mode
68
- def search_name; end
69
- end.tap { |klass| klass.searches *allowed_options }
70
- }
71
- let(:provided_options) { {name: 'Reese Roper', krazy_mode: true} }
72
-
73
- it "sets all the provided values" do
74
- expect(search.name).to eq('Reese Roper')
75
- expect(search.krazy_mode).to eq(true)
76
- end
77
-
78
- it "only lists options for the values corresponding to search methods" do
79
- expect(search.options).to eq({name: 'Reese Roper'})
80
- end
81
-
82
- end
83
-
84
- end
85
-
86
- end
87
-
88
- context "when the search class has defaults" do
89
-
90
- let(:allowed_options) { [:name, :age] }
91
- let(:search_class) {
92
- Named::Class.new('ExampleSearch', described_class) do
93
-
94
- def initialize(options)
95
- super
96
- self.name ||= 'Dennis'
97
- self.age ||= 37
98
- end
99
-
100
- def search_name; end
101
- def search_age; end
102
-
103
- end.tap { |klass| klass.searches *allowed_options }
104
- }
105
-
106
- context "and there were no values given" do
107
-
108
- let(:provided_options) { Hash.new }
109
-
110
- it "uses the defaults for its accessors" do
111
- expect(search.name).to eq('Dennis')
112
- expect(search.age).to eq(37)
113
- end
114
-
115
- it "uses the defaults for its options hash" do
116
- expect(search.options).to eq({name: 'Dennis', age: 37})
117
- end
118
-
119
- end
120
-
121
- context "and values are given" do
122
-
123
- let(:provided_options) { {name: 'Treebeard', age: 'A few thousand'} }
124
-
125
- it "uses the provided values" do
126
- expect(search.name).to eq('Treebeard')
127
- expect(search.age).to eq('A few thousand')
128
- end
129
-
130
- it "uses the provided values for its options hash" do
131
- expect(search.options).to eq({name: 'Treebeard', age: 'A few thousand'})
132
- end
133
-
134
- end
135
-
136
- end
17
+ describe "parsing options" do
137
18
 
19
+ it "makes the raw options available" do
20
+ expect(search.raw_options).to equal(raw_options)
138
21
  end
139
22
 
140
- context "when given invalid options" do
141
-
142
- let(:provided_options) { {genus: 'Mellivora'} }
143
-
144
- it "raises an error explaining that this search class doesn't search the given property" do
145
- expect { search }.to raise_error( Searchlight::Search::UndefinedOption, /ExampleSearch.*genus/)
146
- end
147
-
148
- it "gives the error a readable string representation" do
149
- error = Searchlight::Search::UndefinedOption.new(:badger_height, Array)
150
- expect(error.to_s).to eq(error.message)
151
- end
152
-
153
- context "if the provided option starts with 'search_'" do
154
-
155
- let(:allowed_options) { [:genus] }
156
-
157
- context "and it looks like a valid search option" do
158
-
159
- let(:provided_options) { {search_genus: 'Mellivora'} }
160
-
161
- it "suggests the option name the user may have meant to provide" do
162
- expect { search }.to raise_error( Searchlight::Search::UndefinedOption, /ExampleSearch.*genus.*Did you just mean/)
163
- end
164
-
165
- end
166
-
167
- context "but doesn't look like a valid search option" do
168
-
169
- let(:provided_options) { {search_girth: 'Wee'} }
170
-
171
- it "doesn't suggest an option name" do
172
- begin
173
- search
174
- rescue Searchlight::Search::UndefinedOption => exception
175
- expect(exception.message.match(/Did you just mean/)).to be_nil
176
- end
177
- end
178
-
179
- end
180
-
181
- end
182
-
23
+ it "returns only useful values as `options`" do
24
+ expect(search.options).to eq(
25
+ title_like: "Mere Christianity",
26
+ "author_name_like" => "Lew",
27
+ book_thickness: "smallish",
28
+ in_print: "either",
29
+ tags: ["fancy"],
30
+ )
183
31
  end
184
32
 
185
- end
186
-
187
- describe "search_on" do
188
-
189
- context "when an explicit search target was provided and it's not a proc" do
190
-
191
- let(:example_search_target) { "Bobby Fischer" }
192
-
193
- before :each do
194
- search_class.search_on example_search_target
195
- end
196
-
197
- it "makes the object accessible via `search_target`" do
198
- expect(search_class.search_target).to eq(example_search_target)
199
- end
200
-
201
- it "makes the search target available to its children" do
202
- expect(SpiffyAccountSearch.search_target).to be(MockModel)
203
- end
204
-
205
- it "allows the children to set their own search target" do
206
- klass = Class.new(SpiffyAccountSearch) { search_on Array }
207
- expect(klass.search_target).to be(Array)
208
- expect(SpiffyAccountSearch.search_target).to be(MockModel)
209
- end
210
-
211
- end
212
-
213
- context "when an explicit search target was provided and it is a proc" do
214
-
215
- it "calls it in the process of producing search results" do
216
- search = ProcSearch.new(first_name: "Jimmy")
217
- results = search.results
218
- expect(results).to be_a(MockRelation)
219
- expect(results.called_methods).to eq([:some_scope, :where])
220
- end
221
-
222
- it "allows it to refer to a parent class's callable search target" do
223
- search = ChildProcSearch.new(first_name: "Carlos")
224
- results = search.results
225
- expect(results).to be_a(MockRelation)
226
- expect(results.called_methods).to eq([:some_scope, :other_scope, :where])
227
- end
228
-
229
- end
230
-
231
- context "when no explicit search target was provided" do
232
-
233
- let(:search_class) { Named::Class.new('Namespaced::ExampleSearch', described_class) }
234
-
235
- it "guesses the search class based on its own namespaced class name" do
236
- expect(search_class.search_target).to eq(Namespaced::Example)
237
- end
238
-
239
- context "when it can't make a guess as to the search class" do
240
-
241
- let(:search_class) { Named::Class.new('Somekinda::Searchthingy', described_class) }
242
-
243
- it "raises an exception" do
244
- expect{search_class.search_target}.to raise_error(
245
- Searchlight::Search::MissingSearchTarget,
246
- /No search target/
247
- )
248
- end
249
-
250
- end
251
-
252
- context "when it tries to guess the search class but fails" do
253
-
254
- let(:search_class) { Named::Class.new('NonExistentObjectSearch', described_class) }
255
-
256
- it "raises an exception" do
257
- expect{search_class.search_target}.to raise_error(
258
- Searchlight::Search::MissingSearchTarget,
259
- /No search target.*uninitialized constant.*NonExistentObject/
260
- )
261
- end
262
-
263
- end
264
-
265
- end
266
-
267
- end
268
-
269
- describe "individual option accessors" do
270
-
271
- describe "the accessors module" do
272
-
273
- before :each do
274
- search_class.searches :foo
275
- search_class.searches :bar
276
- search_class.searches :stuff
277
- end
278
-
279
- it "includes exactly one SearchlightAccessors module for this class" do
280
- accessors_modules = search_class.ancestors.select {|a| a.name =~ /\ASearchlightAccessors/ }
281
- expect(accessors_modules.length).to eq(1)
282
- expect(accessors_modules.first).to be_a(Named::Module)
283
- end
284
- end
285
-
286
- describe "value accessors" do
287
-
288
- let(:allowed_options) { [:beak_color] }
289
- let(:provided_options) { {beak_color: 'mauve'} }
290
-
291
- it "provides an getter for the value" do
292
- search_class.searches :beak_color
293
- expect(search.beak_color).to eq('mauve')
294
- end
295
-
296
- it "provides an setter for the value" do
297
- search_class.searches :beak_color
298
- search.beak_color = 'turquoise'
299
- expect(search.beak_color).to eq('turquoise')
300
- end
301
-
302
- end
303
-
304
- describe "boolean accessors" do
305
-
306
- let(:provided_options) { {has_beak: has_beak} }
307
-
308
- before :each do
309
- search_class.searches :has_beak
310
- end
311
-
312
- {
313
- 'yeppers' => true,
314
- 1 => true,
315
- '1' => true,
316
- 15 => true,
317
- 'true' => true,
318
- 0 => false,
319
- '0' => false,
320
- '' => false,
321
- ' ' => false,
322
- nil => false,
323
- 'false' => false
324
- }.each do |input, output|
325
-
326
- describe input.inspect do
327
-
328
- let(:has_beak) { input }
329
-
330
- it "becomes boolean #{output}" do
331
- expect(search.has_beak?).to eq(output)
332
- end
333
-
334
- end
335
-
336
- end
337
-
33
+ it "knows which of the `options` have matching search_ methods" do
34
+ expect(search.options_with_search_methods).to eq(
35
+ title_like: "search_title_like",
36
+ "author_name_like" => "search_author_name_like",
37
+ in_print: "search_in_print",
38
+ )
338
39
  end
339
-
340
40
  end
341
41
 
342
- describe "search" do
42
+ describe "querying" do
343
43
 
344
- let(:search) { AccountSearch.new }
345
-
346
- it "is initialized with the search_target" do
347
- expect(search.search).to eq(MockModel)
348
- end
349
-
350
- end
351
-
352
- describe "results" do
353
-
354
- let(:search) { AccountSearch.new(paid_amount: 50, business_name: "Rod's Meat Shack", other_attribute: 'whatevs') }
355
-
356
- it "builds a search by calling each search method that corresponds to a provided option" do
357
- search.should_receive(:search_paid_amount).and_call_original
358
- search.should_receive(:search_business_name).and_call_original
359
- # Can't do `.should_not_receive(:search_other_attribute)` because the expectation defines a method which would get called.
360
- search.results
361
- expect(search.search.called_methods).to eq(2.times.map { :where })
362
- end
363
-
364
- it "returns the search" do
365
- expect(search.results).to eq(search.search)
44
+ it "builds results by running all methods matching its options" do
45
+ expect(search).to receive(:search_title_like).and_call_original
46
+ expect(search).to receive(:search_author_name_like).and_call_original
47
+ expect(search.results.called_methods).to eq([:all, :order, :merge, :joins, :merge])
366
48
  end
367
49
 
368
50
  it "only runs the search once" do
369
- search.should_receive(:run).once.and_call_original
51
+ expect(search).to receive(:run).once.and_call_original
370
52
  2.times { search.results }
371
53
  end
372
54
 
373
55
  end
374
56
 
375
- describe "run" do
376
-
377
- let(:search_class) {
378
- Named::Class.new('TinyBs', described_class) do
379
- search_on Object
380
- searches :bits, :bats, :bots
381
-
382
- def search_bits; end
383
- def search_bats; end
384
- def search_bots; end
385
-
386
- end
387
- }
388
-
389
- let(:provided_options) { {bits: ' ', bats: nil, bots: false} }
390
-
391
- it "only runs search methods that have real values to search on" do
392
- search.should_not_receive(:search_bits)
393
- search.should_not_receive(:search_bats)
394
- search.should_receive(:search_bots)
395
- search.send(:run)
396
- end
397
-
57
+ it "has an 'explain' method to show how it builds its query" do
58
+ expect(search.explain).to be_a(String)
398
59
  end
399
60
 
400
61
  end