searchlight 1.1.0 → 1.2.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 +8 -8
- data/.gitignore +2 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +1 -0
- data/README.md +12 -4
- data/lib/searchlight/search.rb +17 -17
- data/lib/searchlight/version.rb +1 -1
- data/spec/searchlight/adapters/active_record_spec.rb +0 -4
- data/spec/searchlight/search_spec.rb +71 -36
- data/spec/spec_helper.rb +2 -0
- data/spec/support/account_search.rb +1 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
MmFhNGNkYmJmZDdmMjc5NjJjODI3NzZmN2Q0ODY5NjkyNzYwZjhiZQ==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
YzRlZTAyZTZiN2UzOTM3NWU1YTAyZGRkNDM1YjUwMTA0NTAxMGU4ZQ==
|
7
7
|
!binary "U0hBNTEy":
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
ZTY3MjgyOGI3Y2FjYTFhMzlhZGVhMGQyYmNiYTRmZDI1Nzc3MzY2YjU1Mzcw
|
10
|
+
M2Y4YjFmNDg3YmEzZmUyNWM0ZGYwOTNkZTYyODkyZGNmZTU4NTUyMWQwNmQ3
|
11
|
+
M2U3NDBkOTM4OTRhZGFiNTNhMDQxOTdlYmFiZTEwMTQ1MzlmYjI=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
MDRkNDY3N2Y4Mzc4Mjc2NGZjOGYxYWZlMjc3MjZmYzM4ZmFlYTE0NzMyYWIy
|
14
|
+
YTQwZjhiNjQwM2I3OTI3Zjk3NTA4MzQ1ZTM1NTE3Mzk4NTliMDc1NWVkODRm
|
15
|
+
YzZmZTg0YmUzZDgzMDI3YjYzN2RmZjFiYTBiM2EwMmIyNTUyNzc=
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,11 @@
|
|
2
2
|
|
3
3
|
Searchlight does its best to use [semantic versioning](http://semver.org).
|
4
4
|
|
5
|
+
## v1.2.0
|
6
|
+
|
7
|
+
- Provide `options` accessor that returns all options considered non-blank
|
8
|
+
- Slightly nicer errors when passing invalid options
|
9
|
+
|
5
10
|
## v1.1.0
|
6
11
|
|
7
12
|
ActiveRecord adapter ensures that searches return a relation, even if no options are given
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -4,8 +4,11 @@ Searchlight helps you build searches from options via Ruby methods that you writ
|
|
4
4
|
|
5
5
|
Searchlight can work with any ORM or object that can build a query using chained methods (eg, ActiveRecord's `.where(...).where(...).limit(...)`). It comes with modules for integrating with ActiveRecord and ActionView, but can easily be used in any Ruby program.
|
6
6
|
|
7
|
-
[](https://rubygems.org/gems/searchlight)
|
8
8
|
[](https://codeclimate.com/github/nathanl/searchlight)
|
9
|
+
[](https://travis-ci.org/nathanl/searchlight)
|
10
|
+
[](https://coveralls.io/r/nathanl/searchlight?branch=master)
|
11
|
+
[](https://gemnasium.com/nathanl/searchlight)
|
9
12
|
|
10
13
|
## Overview
|
11
14
|
|
@@ -103,7 +106,7 @@ end
|
|
103
106
|
|
104
107
|
### Accessors
|
105
108
|
|
106
|
-
For each search option, Searchlight defines two accessors: one for a value, and one for a boolean.
|
109
|
+
For each search option you allow, Searchlight defines two accessors: one for a value, and one for a boolean.
|
107
110
|
|
108
111
|
For example, if your class `searches :awesomeness` and gets instantiated like:
|
109
112
|
|
@@ -143,11 +146,12 @@ class PersonSearch < Searchlight::Search
|
|
143
146
|
end
|
144
147
|
```
|
145
148
|
|
149
|
+
Additionally, each search instance has an `options` accessor, which will have all the usable options with which it was instantiated. This excludes empty collections, blank strings, `nil`, etc. These usable options will be used in determining which search methods to run.
|
150
|
+
|
146
151
|
### Defining Defaults
|
147
152
|
|
148
153
|
Set defaults using plain Ruby. These can be used to prefill a form or to assume what the user didn't specify.
|
149
154
|
|
150
|
-
|
151
155
|
```ruby
|
152
156
|
|
153
157
|
class CitySearch < Searchlight::Search
|
@@ -252,6 +256,9 @@ Searchlight plays nicely with Rails forms. All search options and any `attr_acce
|
|
252
256
|
= f.select :continent, ['Africa', 'Asia', 'Europe'], include_blank: true
|
253
257
|
|
254
258
|
= f.submit "Search"
|
259
|
+
|
260
|
+
- @results.each do |city|
|
261
|
+
= render 'city'
|
255
262
|
```
|
256
263
|
|
257
264
|
### Controllers
|
@@ -263,7 +270,8 @@ As long as your form submits options your search understands, you can easily hoo
|
|
263
270
|
class OrdersController < ApplicationController
|
264
271
|
|
265
272
|
def index
|
266
|
-
@
|
273
|
+
@search = OrderSearch.new(search_params) # For use in a form
|
274
|
+
@results = @search.results # For display along with form
|
267
275
|
end
|
268
276
|
|
269
277
|
protected
|
data/lib/searchlight/search.rb
CHANGED
@@ -2,6 +2,8 @@ module Searchlight
|
|
2
2
|
class Search
|
3
3
|
extend DSL
|
4
4
|
|
5
|
+
attr_accessor :options
|
6
|
+
|
5
7
|
def self.search_target
|
6
8
|
return @search_target if defined?(@search_target)
|
7
9
|
return superclass.search_target if superclass.respond_to?(:search_target) && superclass != Searchlight::Search
|
@@ -9,9 +11,7 @@ module Searchlight
|
|
9
11
|
end
|
10
12
|
|
11
13
|
def initialize(options = {})
|
12
|
-
options
|
13
|
-
rescue NoMethodError => e
|
14
|
-
raise UndefinedOption.new(e.name, self.class.name)
|
14
|
+
filter_and_mass_assign(options)
|
15
15
|
end
|
16
16
|
|
17
17
|
def search
|
@@ -45,26 +45,25 @@ module Searchlight
|
|
45
45
|
@search_target = value
|
46
46
|
end
|
47
47
|
|
48
|
-
def
|
49
|
-
|
48
|
+
def filter_and_mass_assign(provided_options)
|
49
|
+
self.options = provided_options.reject { |key, value| is_blank?(value) }
|
50
|
+
begin
|
51
|
+
options.each { |key, value| public_send("#{key}=", value) } if options && options.any?
|
52
|
+
rescue NoMethodError => e
|
53
|
+
raise UndefinedOption.new(e.name, self)
|
54
|
+
end
|
50
55
|
end
|
51
56
|
|
52
57
|
def run
|
53
|
-
|
54
|
-
new_search =
|
58
|
+
options.each do |option_name, value|
|
59
|
+
new_search = public_send("search_#{option_name}") if respond_to?("search_#{option_name}")
|
55
60
|
self.search = new_search unless new_search.nil?
|
56
61
|
end
|
57
62
|
search
|
58
63
|
end
|
59
64
|
|
60
|
-
def run_search_method(method_name)
|
61
|
-
option_value = instance_variable_get("@#{method_name.sub(/\Asearch_/, '')}")
|
62
|
-
option_value = option_value.reject { |item| blank_value?(item) } if option_value.respond_to?(:reject)
|
63
|
-
public_send(method_name) unless blank_value?(option_value)
|
64
|
-
end
|
65
|
-
|
66
65
|
# Note that false is not blank
|
67
|
-
def
|
66
|
+
def is_blank?(value)
|
68
67
|
(value.respond_to?(:empty?) && value.empty?) || value.nil? || value.to_s.strip == ''
|
69
68
|
end
|
70
69
|
|
@@ -74,12 +73,13 @@ module Searchlight
|
|
74
73
|
|
75
74
|
attr_accessor :message
|
76
75
|
|
77
|
-
def initialize(option_name,
|
76
|
+
def initialize(option_name, search)
|
78
77
|
option_name = option_name.to_s.sub(/=\Z/, '')
|
79
|
-
self.message = "#{
|
78
|
+
self.message = "#{search.class.name} doesn't search '#{option_name}' or have an accessor for that property."
|
80
79
|
if option_name.start_with?('search_')
|
80
|
+
method_maybe_intended = option_name.sub(/\Asearch_/, '')
|
81
81
|
# Gee golly, I'm so helpful!
|
82
|
-
self.message << " Did you just mean '#{
|
82
|
+
self.message << " Did you just mean '#{method_maybe_intended}'?" if search.respond_to?("#{method_maybe_intended}=")
|
83
83
|
end
|
84
84
|
end
|
85
85
|
|
data/lib/searchlight/version.rb
CHANGED
@@ -26,10 +26,6 @@ describe 'Searchlight::Adapters::ActiveRecord', adapter: true do
|
|
26
26
|
expect(search_class.new).to respond_to(:search_elephants)
|
27
27
|
end
|
28
28
|
|
29
|
-
it "adds search_elephants to the search_methods array" do
|
30
|
-
expect(search_instance.send(:search_methods)).to include('search_elephants')
|
31
|
-
end
|
32
|
-
|
33
29
|
it "defines search methods that call where on the search target" do
|
34
30
|
search_instance.results
|
35
31
|
expect(search_instance.search.called_methods).to include(:where)
|
@@ -2,33 +2,86 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe Searchlight::Search do
|
4
4
|
|
5
|
-
let(:search_class)
|
6
|
-
let(:
|
7
|
-
let(:
|
5
|
+
let(:search_class) { Named::Class.new('ExampleSearch', described_class).tap { |klass| klass.searches *allowed_options } }
|
6
|
+
let(:allowed_options) { Hash.new }
|
7
|
+
let(:provided_options) { Hash.new }
|
8
|
+
let(:search) { search_class.new(provided_options) }
|
8
9
|
|
9
10
|
describe "initializing" do
|
10
11
|
|
11
|
-
|
12
|
+
describe "mass-assigning provided options" do
|
13
|
+
|
14
|
+
let(:allowed_options) { [:beak_color] }
|
15
|
+
let(:provided_options) { {beak_color: 'mauve'} }
|
16
|
+
|
17
|
+
it "mass-assigns provided options" do
|
18
|
+
search_class.searches :beak_color
|
19
|
+
expect(search.beak_color).to eq('mauve')
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "screening options" do
|
25
|
+
|
26
|
+
let(:allowed_options) { [:name, :description, :categories, :nicknames] }
|
27
|
+
|
28
|
+
context "when non-empty options are provided" do
|
29
|
+
|
30
|
+
let(:provided_options) { {name: 'Roy', description: 'Ornry', categories: %w[mammal moonshiner], nicknames: %w[Slim Bubba]} }
|
31
|
+
|
32
|
+
it "adds them to the options accessor" do
|
33
|
+
expect(search.options).to eq(provided_options)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
context "when some provided options are empty" do
|
39
|
+
|
40
|
+
let(:provided_options) { {name: 'Roy', description: '', categories: %w[mammal moonshiner], nicknames: []} }
|
41
|
+
|
42
|
+
it "does not add them to the options accessor" do
|
43
|
+
expect(search.options).to eq(name: 'Roy', categories: %w[mammal moonshiner])
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
12
48
|
|
13
|
-
it "mass-assigns provided options" do
|
14
|
-
search_class.searches :beak_color
|
15
|
-
expect(search.beak_color).to eq('mauve')
|
16
49
|
end
|
17
50
|
|
18
51
|
describe "handling invalid options" do
|
19
52
|
|
20
|
-
let(:
|
53
|
+
let(:provided_options) { {genus: 'Mellivora'} }
|
21
54
|
|
22
55
|
it "raises an error explaining that this search class doesn't search the given property" do
|
23
56
|
expect { search }.to raise_error( Searchlight::Search::UndefinedOption, /ExampleSearch.*genus/)
|
24
57
|
end
|
25
58
|
|
26
|
-
context "if the option starts with 'search_'" do
|
59
|
+
context "if the provided option starts with 'search_'" do
|
60
|
+
|
61
|
+
let(:allowed_options) { [:genus] }
|
62
|
+
|
63
|
+
context "and it looks like a valid search option" do
|
27
64
|
|
28
|
-
|
65
|
+
let(:provided_options) { {search_genus: 'Mellivora'} }
|
66
|
+
|
67
|
+
it "suggests the option name the user may have meant to provide" do
|
68
|
+
expect { search }.to raise_error( Searchlight::Search::UndefinedOption, /ExampleSearch.*genus.*Did you just mean/)
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
context "but doesn't look like a valid search option" do
|
74
|
+
|
75
|
+
let(:provided_options) { {search_girth: 'Wee'} }
|
76
|
+
|
77
|
+
it "doesn't suggest an option name" do
|
78
|
+
begin
|
79
|
+
search
|
80
|
+
rescue Searchlight::Search::UndefinedOption => exception
|
81
|
+
expect(exception.message.match(/Did you just mean/)).to be_nil
|
82
|
+
end
|
83
|
+
end
|
29
84
|
|
30
|
-
it "suggests the option name the user may have meant to provide" do
|
31
|
-
expect { search }.to raise_error( Searchlight::Search::UndefinedOption, /ExampleSearch.*genus.*Did you just mean/)
|
32
85
|
end
|
33
86
|
|
34
87
|
end
|
@@ -101,27 +154,6 @@ describe Searchlight::Search do
|
|
101
154
|
|
102
155
|
end
|
103
156
|
|
104
|
-
describe "search_methods" do
|
105
|
-
|
106
|
-
let(:search_class) {
|
107
|
-
Named::Class.new('ExampleSearch', described_class) do
|
108
|
-
def search_bees
|
109
|
-
end
|
110
|
-
|
111
|
-
def search_bats
|
112
|
-
end
|
113
|
-
|
114
|
-
def search_bees
|
115
|
-
end
|
116
|
-
end
|
117
|
-
}
|
118
|
-
|
119
|
-
it "keeps a unique list of the search methods" do
|
120
|
-
expect(search.send(:search_methods).map(&:to_s).sort).to eq(['search_bats', 'search_bees'])
|
121
|
-
end
|
122
|
-
|
123
|
-
end
|
124
|
-
|
125
157
|
describe "search options" do
|
126
158
|
|
127
159
|
describe "accessors" do
|
@@ -154,7 +186,7 @@ describe Searchlight::Search do
|
|
154
186
|
|
155
187
|
describe "accessing search options as booleans" do
|
156
188
|
|
157
|
-
let(:
|
189
|
+
let(:provided_options) { {fishies: fishies} }
|
158
190
|
|
159
191
|
before :each do
|
160
192
|
search_class.searches :fishies
|
@@ -202,9 +234,12 @@ describe Searchlight::Search do
|
|
202
234
|
|
203
235
|
describe "results" do
|
204
236
|
|
205
|
-
let(:search) { AccountSearch.new(paid_amount: 50, business_name: "Rod's Meat Shack") }
|
237
|
+
let(:search) { AccountSearch.new(paid_amount: 50, business_name: "Rod's Meat Shack", other_attribute: 'whatevs') }
|
206
238
|
|
207
|
-
it "builds a search by calling
|
239
|
+
it "builds a search by calling each search method that corresponds to a provided option" do
|
240
|
+
search.should_receive(:search_paid_amount).and_call_original
|
241
|
+
search.should_receive(:search_business_name).and_call_original
|
242
|
+
# Can't do `.should_not_receive(:search_other_attribute)` because the expectation defines a method which would get called.
|
208
243
|
search.results
|
209
244
|
expect(search.search.called_methods).to eq(2.times.map { :where })
|
210
245
|
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: searchlight
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nathan Long
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-06-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: named
|