searchlight 0.9.1 → 1.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 +6 -14
- data/CHANGELOG.md +7 -0
- data/README.md +58 -7
- data/lib/searchlight/search.rb +22 -2
- data/lib/searchlight/version.rb +1 -1
- data/spec/searchlight/search_spec.rb +65 -15
- data/spec/support/mock_model.rb +4 -0
- metadata +8 -8
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
metadata.gz: !binary |-
|
9
|
-
OGM2NjZmNTc2OTI3MjdkMmNlNTcwNzE2Mjk5Mzk3YmIzMzVmMjNjYTVmMGFi
|
10
|
-
ODhkYjQ2ZmNiNzJlNDNlYzhlOTVjMDIxN2M5ZGE4NDU5ZGI4NmYyN2JjMzI2
|
11
|
-
NWFmYWI0NDYwOTg4YjI1ZjhkOTJiNmQyNzkzNDRkMzc2ZjM2MjM=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
M2JiOGE2ODJiNjhlOTM0NDBiNGVkZDBlZDE5MDNiMDRjMGRjOTQ5MzViZmM4
|
14
|
-
YWZiOTVkNDFmZTdiZmRkMTM1ZjE3MGM2ZjBlMzFlMGE2ODM3NGM0OTBiN2I0
|
15
|
-
NDFlOTE0NzM3MjJkOGFiYmY5ZDFkMzBmNTdkNzcyZmE5M2U5YTE=
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 65909cc6d8f99929aa4c76ba6a52e23dd8b85f59
|
4
|
+
data.tar.gz: 8ed82b1eab66fe78fcfc2b7b1630a8cbd2e2dcf3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fd26998a2330f3b034cdad89efe07ab3a52e0125db072da03533aa0038e091240baf0af3428ddc84de5b7e7e558b9a0c5fe6b095cf9a30acee503dbd359cb27e
|
7
|
+
data.tar.gz: 62a9b2a4ae2e8e5d8b28b996199b500c210f63ef53805e1c5ce491b7273832a5aed11155ff86a83dc7c4f4d5f6d66aa8a88b1db7d524dd2ca24d622c769fa0ea
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,13 @@
|
|
2
2
|
|
3
3
|
Searchlight does its best to use [semantic versioning](http://semver.org).
|
4
4
|
|
5
|
+
## v1.0.0
|
6
|
+
|
7
|
+
- If no search target is given, search class attempts to guess based on its own name
|
8
|
+
- All errors that can be raise extend `Searchlight::Error`
|
9
|
+
- Better testing
|
10
|
+
- Still more documentation!
|
11
|
+
|
5
12
|
## v0.9.1
|
6
13
|
|
7
14
|
Bugfix for ActiveRecord adapter
|
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
Searchlight helps you build searches from options via Ruby methods that you write.
|
4
4
|
|
5
|
-
Searchlight can work with any ORM or object that
|
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
|
[](https://travis-ci.org/nathanl/searchlight)
|
8
8
|
[](https://codeclimate.com/github/nathanl/searchlight)
|
@@ -145,6 +145,9 @@ end
|
|
145
145
|
|
146
146
|
### Defining Defaults
|
147
147
|
|
148
|
+
Set defaults using plain Ruby. These can be used to prefill a form or to assume what the user didn't specify.
|
149
|
+
|
150
|
+
|
148
151
|
```ruby
|
149
152
|
|
150
153
|
class CitySearch < Searchlight::Search
|
@@ -152,7 +155,7 @@ class CitySearch < Searchlight::Search
|
|
152
155
|
#...
|
153
156
|
|
154
157
|
def initialize(options = {})
|
155
|
-
super
|
158
|
+
super
|
156
159
|
self.continent ||= 'Asia'
|
157
160
|
end
|
158
161
|
|
@@ -165,6 +168,38 @@ CitySearch.new(continent: 'Europe').results.to_sql
|
|
165
168
|
=> "SELECT `cities`.* FROM `cities` WHERE (`countries`.`continent` = 'Europe')"
|
166
169
|
```
|
167
170
|
|
171
|
+
You can define defaults for boolean attributes if you treat them as "yes/no/either" choices.
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
class AnimalSearch < Searchlight::Search
|
175
|
+
|
176
|
+
search_on Animal
|
177
|
+
|
178
|
+
searches :is_fictional
|
179
|
+
|
180
|
+
def initialize(*args)
|
181
|
+
super
|
182
|
+
self.is_fictional = :either if is_fictional.blank?
|
183
|
+
end
|
184
|
+
|
185
|
+
def search_is_fictional
|
186
|
+
case is_fictional.to_s
|
187
|
+
when 'true' then search.where(fictional: true)
|
188
|
+
when 'false' then search.where(fictional: false)
|
189
|
+
when 'either' then search # unmodified
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
|
195
|
+
AnimalSearch.new(fictional: true).results.to_sql
|
196
|
+
=> "SELECT `animals`.* FROM `animals` WHERE (`fictional` = true)"
|
197
|
+
AnimalSearch.new(fictional: false).results.to_sql
|
198
|
+
=> "SELECT `animals`.* FROM `animals` WHERE (`fictional` = false)"
|
199
|
+
AnimalSearch.new.results.to_sql
|
200
|
+
=> "SELECT `animals`.* FROM `animals`"
|
201
|
+
```
|
202
|
+
|
168
203
|
### Subclassing
|
169
204
|
|
170
205
|
You can subclass an existing search class and support all the same options with a different search target. This may be useful for single table inheritance, for example.
|
@@ -186,9 +221,15 @@ SmallTownSearch.new(country_name_like: 'Norfolk').results.to_sql
|
|
186
221
|
=> "SELECT `cities`.* FROM `cities` WHERE (`cities`.`population` < 1000) AND (`countries`.`name` LIKE '%Norfolk%')"
|
187
222
|
```
|
188
223
|
|
224
|
+
### Dependent Options
|
225
|
+
|
226
|
+
To allow search options that don't trigger searches directly, just use `attr_accessor`.
|
227
|
+
|
189
228
|
## Usage in Rails
|
190
229
|
|
191
|
-
|
230
|
+
### Forms
|
231
|
+
|
232
|
+
Searchlight plays nicely with Rails forms. All search options and any `attr_accessor`s you define can be hooked up to form fields.
|
192
233
|
|
193
234
|
```ruby
|
194
235
|
# app/views/cities/index.html.haml
|
@@ -213,13 +254,23 @@ Searchlight plays nicely with Rails views.
|
|
213
254
|
= f.submit "Search"
|
214
255
|
```
|
215
256
|
|
257
|
+
### Controllers
|
258
|
+
|
216
259
|
As long as your form submits options your search understands, you can easily hook it up in your controller:
|
217
260
|
|
218
261
|
```ruby
|
219
|
-
# app/controllers/
|
220
|
-
class
|
221
|
-
|
222
|
-
|
262
|
+
# app/controllers/orders_controller.rb
|
263
|
+
class OrdersController < ApplicationController
|
264
|
+
|
265
|
+
def index
|
266
|
+
@orders = OrderSearch.new(search_params)
|
267
|
+
end
|
268
|
+
|
269
|
+
protected
|
270
|
+
|
271
|
+
def search_params
|
272
|
+
# Ensure the user can only browse or search their own orders
|
273
|
+
(params[:search]) || {}).merge(user_id: current_user.id)
|
223
274
|
end
|
224
275
|
end
|
225
276
|
```
|
data/lib/searchlight/search.rb
CHANGED
@@ -3,7 +3,9 @@ module Searchlight
|
|
3
3
|
extend DSL
|
4
4
|
|
5
5
|
def self.search_target
|
6
|
-
defined?(@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!
|
7
9
|
end
|
8
10
|
|
9
11
|
def initialize(options = {})
|
@@ -26,6 +28,19 @@ module Searchlight
|
|
26
28
|
|
27
29
|
private
|
28
30
|
|
31
|
+
def self.guess_search_class!
|
32
|
+
if self.name.end_with?('Search')
|
33
|
+
@search_target = name.sub(/Search\z/, '').split('::').inject(Kernel, &:const_get)
|
34
|
+
else
|
35
|
+
raise MissingSearchTarget, "No search target provided via `search_on` and Searchlight can't guess one."
|
36
|
+
end
|
37
|
+
rescue NameError => e
|
38
|
+
if /uninitialized constant/.match(e.message)
|
39
|
+
raise MissingSearchTarget, "No search target provided via `search_on` and Searchlight's guess was wrong. Error: #{e.message}"
|
40
|
+
end
|
41
|
+
raise e
|
42
|
+
end
|
43
|
+
|
29
44
|
def search_methods
|
30
45
|
public_methods.map(&:to_s).select { |m| m.start_with?('search_') }
|
31
46
|
end
|
@@ -49,8 +64,12 @@ module Searchlight
|
|
49
64
|
(value.respond_to?(:empty?) && value.empty?) || value.nil? || value.to_s.strip == ''
|
50
65
|
end
|
51
66
|
|
52
|
-
|
67
|
+
MissingSearchTarget = Class.new(Searchlight::Error)
|
68
|
+
|
69
|
+
class UndefinedOption < Searchlight::Error
|
70
|
+
|
53
71
|
attr_accessor :message
|
72
|
+
|
54
73
|
def initialize(option_name, search_class)
|
55
74
|
option_name = option_name.to_s.sub(/=\Z/, '')
|
56
75
|
self.message = "#{search_class} doesn't search '#{option_name}'."
|
@@ -63,6 +82,7 @@ module Searchlight
|
|
63
82
|
def to_s
|
64
83
|
message
|
65
84
|
end
|
85
|
+
|
66
86
|
end
|
67
87
|
|
68
88
|
end
|
data/lib/searchlight/version.rb
CHANGED
@@ -3,8 +3,8 @@ require 'spec_helper'
|
|
3
3
|
describe Searchlight::Search do
|
4
4
|
|
5
5
|
let(:search_class) { Named::Class.new('ExampleSearch', described_class) }
|
6
|
-
let(:options)
|
7
|
-
let(:search)
|
6
|
+
let(:options) { Hash.new }
|
7
|
+
let(:search) { search_class.new(options) }
|
8
8
|
|
9
9
|
describe "initializing" do
|
10
10
|
|
@@ -23,30 +23,80 @@ describe Searchlight::Search do
|
|
23
23
|
expect { search }.to raise_error( Searchlight::Search::UndefinedOption, /ExampleSearch.*genus/)
|
24
24
|
end
|
25
25
|
|
26
|
+
context "if the option starts with 'search_'" do
|
27
|
+
|
28
|
+
let(:options) { {search_genus: 'Mellivora'} }
|
29
|
+
|
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
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
26
36
|
end
|
27
37
|
|
28
38
|
end
|
29
39
|
|
30
40
|
describe "search_on" do
|
31
41
|
|
32
|
-
|
42
|
+
context "when an explicit search target was provided" do
|
33
43
|
|
34
|
-
|
35
|
-
search_class.search_on search_target
|
36
|
-
end
|
44
|
+
let(:search_target) { "Bobby Fischer" }
|
37
45
|
|
38
|
-
|
39
|
-
|
40
|
-
|
46
|
+
before :each do
|
47
|
+
search_class.search_on search_target
|
48
|
+
end
|
49
|
+
|
50
|
+
it "makes the object accessible via `search_target`" do
|
51
|
+
expect(search_class.search_target).to eq(search_target)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "makes the search target available to its children" do
|
55
|
+
expect(SpiffyAccountSearch.search_target).to be(MockModel)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "allows the children to set their own search target" do
|
59
|
+
klass = Class.new(SpiffyAccountSearch) { search_on Array }
|
60
|
+
expect(klass.search_target).to be(Array)
|
61
|
+
expect(SpiffyAccountSearch.search_target).to be(MockModel)
|
62
|
+
end
|
41
63
|
|
42
|
-
it "makes the search target available to its children" do
|
43
|
-
expect(SpiffyAccountSearch.search_target).to be(MockModel)
|
44
64
|
end
|
45
65
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
66
|
+
context "when no explicit search target was provided" do
|
67
|
+
|
68
|
+
let(:search_class) { Named::Class.new('Namespaced::ExampleSearch', described_class) }
|
69
|
+
|
70
|
+
it "guesses the search class based on its own namespaced class name" do
|
71
|
+
expect(search_class.search_target).to eq(Namespaced::Example)
|
72
|
+
end
|
73
|
+
|
74
|
+
context "when it can't make a guess as to the search class" do
|
75
|
+
|
76
|
+
let(:search_class) { Named::Class.new('Somekinda::Searchthingy', described_class) }
|
77
|
+
|
78
|
+
it "raises an exception" do
|
79
|
+
expect{search_class.search_target}.to raise_error(
|
80
|
+
Searchlight::Search::MissingSearchTarget,
|
81
|
+
/No search target/
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
context "when it tries to guess the search class but fails" do
|
88
|
+
|
89
|
+
let(:search_class) { Named::Class.new('NonExistentObjectSearch', described_class) }
|
90
|
+
|
91
|
+
it "raises an exception" do
|
92
|
+
expect{search_class.search_target}.to raise_error(
|
93
|
+
Searchlight::Search::MissingSearchTarget,
|
94
|
+
/No search target.*uninitialized constant.*NonExistentObject/
|
95
|
+
)
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
50
100
|
end
|
51
101
|
|
52
102
|
end
|
data/spec/support/mock_model.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: 0.
|
4
|
+
version: 1.0.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-05-17 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: named
|
@@ -57,28 +57,28 @@ dependencies:
|
|
57
57
|
name: rake
|
58
58
|
requirement: !ruby/object:Gem::Requirement
|
59
59
|
requirements:
|
60
|
-
- -
|
60
|
+
- - '>='
|
61
61
|
- !ruby/object:Gem::Version
|
62
62
|
version: '0'
|
63
63
|
type: :development
|
64
64
|
prerelease: false
|
65
65
|
version_requirements: !ruby/object:Gem::Requirement
|
66
66
|
requirements:
|
67
|
-
- -
|
67
|
+
- - '>='
|
68
68
|
- !ruby/object:Gem::Version
|
69
69
|
version: '0'
|
70
70
|
- !ruby/object:Gem::Dependency
|
71
71
|
name: rails
|
72
72
|
requirement: !ruby/object:Gem::Requirement
|
73
73
|
requirements:
|
74
|
-
- -
|
74
|
+
- - '>='
|
75
75
|
- !ruby/object:Gem::Version
|
76
76
|
version: '3'
|
77
77
|
type: :development
|
78
78
|
prerelease: false
|
79
79
|
version_requirements: !ruby/object:Gem::Requirement
|
80
80
|
requirements:
|
81
|
-
- -
|
81
|
+
- - '>='
|
82
82
|
- !ruby/object:Gem::Version
|
83
83
|
version: '3'
|
84
84
|
- !ruby/object:Gem::Dependency
|
@@ -154,12 +154,12 @@ require_paths:
|
|
154
154
|
- lib
|
155
155
|
required_ruby_version: !ruby/object:Gem::Requirement
|
156
156
|
requirements:
|
157
|
-
- -
|
157
|
+
- - '>='
|
158
158
|
- !ruby/object:Gem::Version
|
159
159
|
version: '0'
|
160
160
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
161
161
|
requirements:
|
162
|
-
- -
|
162
|
+
- - '>='
|
163
163
|
- !ruby/object:Gem::Version
|
164
164
|
version: '0'
|
165
165
|
requirements: []
|