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.
- 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
|