searchlight 3.1.1 → 4.0.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.
@@ -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