searchlight 3.1.1 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -2
- data/CHANGELOG.md +18 -1
- data/CODE_OF_CONDUCT.md +9 -0
- data/README.md +86 -130
- data/Rakefile +8 -0
- data/TODO.md +2 -1
- data/lib/searchlight.rb +4 -9
- data/lib/searchlight/adapters/action_view.rb +2 -4
- data/lib/searchlight/options.rb +27 -0
- data/lib/searchlight/search.rb +50 -98
- data/lib/searchlight/version.rb +1 -1
- data/searchlight.gemspec +2 -4
- data/spec/searchlight/adapters/action_view_spec.rb +10 -7
- data/spec/searchlight/options_spec.rb +104 -0
- data/spec/searchlight/search_spec.rb +34 -373
- data/spec/spec_helper.rb +1 -4
- data/spec/support/book_search.rb +46 -0
- metadata +13 -28
- data/lib/searchlight/dsl.rb +0 -33
- data/spec/support/account_search.rb +0 -16
- data/spec/support/proc_search.rb +0 -17
- data/spec/support/spiffy_account_search.rb +0 -2
@@ -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
|
data/lib/searchlight/search.rb
CHANGED
@@ -1,117 +1,69 @@
|
|
1
|
-
|
2
|
-
class Search
|
3
|
-
extend DSL
|
1
|
+
require_relative "options"
|
4
2
|
|
5
|
-
|
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
|
-
|
5
|
+
SEARCH_METHOD_PATTERN = /\Asearch_(?<option>.*)/
|
39
6
|
|
40
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
91
|
-
target.is_a?(Proc)
|
92
|
-
end
|
28
|
+
def options
|
29
|
+
Searchlight::Options.excluding_empties(raw_options)
|
30
|
+
end
|
93
31
|
|
94
|
-
|
32
|
+
def empty?(value)
|
33
|
+
Searchlight::Options.empty?(value)
|
34
|
+
end
|
95
35
|
|
96
|
-
|
36
|
+
def checked?(value)
|
37
|
+
Searchlight::Options.checked?(value)
|
38
|
+
end
|
97
39
|
|
98
|
-
|
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
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
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
|
data/lib/searchlight/version.rb
CHANGED
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.
|
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.
|
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
|
1
|
+
require "spec_helper"
|
2
2
|
|
3
|
-
describe
|
3
|
+
describe "Searchlight::Adapters::ActionView", type: :feature do
|
4
4
|
|
5
5
|
let(:view) { ::ActionView::Base.new }
|
6
|
-
let(:search) {
|
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
|
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.
|
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(:
|
20
|
+
f.text_field(:title_like)
|
20
21
|
end
|
21
22
|
|
22
|
-
expect(form).to have_selector(
|
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(:
|
6
|
-
|
7
|
-
|
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(:
|
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
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
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
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
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 "
|
42
|
+
describe "querying" do
|
343
43
|
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
expect(search.
|
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.
|
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
|
-
|
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
|