searchlight 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
[![
|
7
|
+
[![Gem Version](https://badge.fury.io/rb/searchlight.png)](https://rubygems.org/gems/searchlight)
|
8
8
|
[![Code Climate](https://codeclimate.com/github/nathanl/searchlight.png)](https://codeclimate.com/github/nathanl/searchlight)
|
9
|
+
[![Build Status](https://api.travis-ci.org/nathanl/searchlight.png?branch=master)](https://travis-ci.org/nathanl/searchlight)
|
10
|
+
[![Coverage Status](https://coveralls.io/repos/nathanl/searchlight/badge.png?branch=master)](https://coveralls.io/r/nathanl/searchlight?branch=master)
|
11
|
+
[![Dependency Status](https://gemnasium.com/nathanl/searchlight.png)](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
|