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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ad62f9410f019b9c6bb09ce5e1e6b1e804ee152c
|
4
|
+
data.tar.gz: 154208ad780633fff9670fd0a174ae3d46ae228a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0aba3b57deac310d31274584c06ecd8075c2c709e99b8ca2190dd7fffadee25cd31a78b162ca750c5b3ea054d134b61e385b8d7f0eacd12da07f039c530dbcfd
|
7
|
+
data.tar.gz: dfb54abe4eadcb4a3657a37225315ed0e8c1c79d65e035b5655f306fb329b2e9190529f50e2ca6fd3c98dd9f4b23bc2fe4c0ab91316708e174f6e8c92e012a66
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,23 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
-
Searchlight does its best to use [semantic versioning](http://semver.org).
|
3
|
+
Searchlight does its best to use [semantic versioning](http://semver.org), for the maintainers' best guess of ["what is a breaking change?"](https://xkcd.com/1172/).
|
4
|
+
|
5
|
+
## Unreleased
|
6
|
+
|
7
|
+
Nothing
|
8
|
+
|
9
|
+
## v4.0.0 - 2015-10-28
|
10
|
+
|
11
|
+
Removed the DSL methods to simplify some things. This is a breaking change, but I think the upgrade will be pretty easy. See below for details.
|
12
|
+
|
13
|
+
- Removed all DSL methods.
|
14
|
+
- `search_on` is now `def base_query`. Defining the base query in an instance method removes the need for procs and makes modification in subclasses as simple as `super`. Note that you *must* define `base_query` - Searchlight will no longer guess your search class based on the name of the search class. If you want such magic, [here it was](https://github.com/nathanl/searchlight/blob/v3.1.1/lib/searchlight/search.rb#L50).
|
15
|
+
- `searches` is removed. If your search has a public method like `search_title`, Searchlight will know to hand the `title` option to that method, and will define a `.title` reader for the option's value. This is slightly magical, but makes your code less repetitive.
|
16
|
+
- Option-grabbing methods like `.title` and `.title?` are gone, in favor of `options[:title]` and `checked?(options[:title])`.
|
17
|
+
- `checked?` interprets `'0'` and `'false'` as false
|
18
|
+
- `empty?` interprets empty arrays and hashes as empty, as well as empty or whitespace-only strings. It's used to filter the options that get passed to your search methods.
|
19
|
+
- `explain` tells you exactly how searchlight interpreted the options a search was given. (Depending on your ORM, you might also want to call `.sql` or `.to_sql` on `search.results` for further debugging.)
|
20
|
+
- `Searchlight::Adapters::ActionView` adapter must now be explicitly required and included.
|
4
21
|
|
5
22
|
## v3.1.1
|
6
23
|
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
Searchlight is a community effort. Everyone should feel welcome to contribute to it, ask questions, or make suggestions.
|
4
|
+
|
5
|
+
All contributors and maintainers must behave in a respectful and kind manner when working on this project. Harassment, sexual language or images, insults, and otherwise offensive behavior will not be tolerated.
|
6
|
+
|
7
|
+
Any contributions that violate this standard may be edited or deleted, and the offending contributor may have their right to participate revoked.
|
8
|
+
|
9
|
+
If you see behavior that violates this standard, please either open an issue or email one of the project maintainers.
|
data/README.md
CHANGED
@@ -15,17 +15,24 @@ An [introductory video](https://vimeo.com/69179161), [the demo app it uses](http
|
|
15
15
|
|
16
16
|
## Overview
|
17
17
|
|
18
|
-
|
18
|
+
Searchlight's main use is to support search forms in web applications.
|
19
|
+
|
20
|
+
Searchlight doesn't write queries for you. What it does do is:
|
21
|
+
|
22
|
+
- Give you an object with which you can build a search form (eg, using `form_for` in Rails)
|
23
|
+
- Give you a sensible place to put your query logic
|
24
|
+
- Decide which parts of the search to run based on what the user submitted (eg, if they didn't fill in a "first name", don't do the `WHERE first_name =` part)
|
19
25
|
|
20
26
|
For example, if you have a Searchlight search class called `YetiSearch`, and you instantiate it like this:
|
21
27
|
|
22
28
|
```ruby
|
23
29
|
search = YetiSearch.new(
|
24
|
-
|
30
|
+
# or params[:yeti_search]
|
31
|
+
"active" => true, "name" => "Jimmy", "location_in" => %w[NY LA]
|
25
32
|
)
|
26
33
|
```
|
27
34
|
|
28
|
-
... calling `results`
|
35
|
+
... calling `search.results` will build a search by calling the methods `search_active`, `search_name`, and `search_location_in` on your `YetiSearch`, assuming that you've defined them. (If you do it again but omit `"name"`, it won't call `search_name`.)
|
29
36
|
|
30
37
|
The `results` method will then return the return value of the last search method. If you're using ActiveRecord, this would be an `ActiveRecord::Relation`, and you can then call `each` to loop through the results, `to_sql` to get the generated query, etc.
|
31
38
|
|
@@ -33,46 +40,46 @@ The `results` method will then return the return value of the last search method
|
|
33
40
|
|
34
41
|
### Search class
|
35
42
|
|
36
|
-
A search class has
|
43
|
+
A search class has two main parts: a `base_query` and some `search_` methods. For example:
|
37
44
|
|
38
45
|
```ruby
|
39
46
|
class PersonSearch < Searchlight::Search
|
40
47
|
|
41
|
-
# The search target; in this case, an ActiveRecord model.
|
42
48
|
# This is the starting point for any chaining we do, and it's what
|
43
49
|
# will be returned if no search options are passed.
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
50
|
+
# In this case, it's an ActiveRecord model.
|
51
|
+
def base_query
|
52
|
+
Person.all # or `.scoped` for ActiveRecord 3
|
53
|
+
end
|
48
54
|
|
49
55
|
# A search method.
|
50
56
|
def search_first_name
|
51
|
-
# If
|
52
|
-
# the
|
53
|
-
|
54
|
-
search.where(first_name: first_name)
|
57
|
+
# If `"first_name"` was the first key in the options_hash,
|
58
|
+
# `search` here will be the base query, namely, `Person.all`.
|
59
|
+
search.where(first_name: options[:first_name])
|
55
60
|
end
|
56
61
|
|
57
62
|
# Another search method.
|
58
63
|
def search_last_name
|
59
|
-
# If
|
60
|
-
# whatever `search_first_name` returned.
|
64
|
+
# If `"last_name"` was the second key in the options_hash,
|
65
|
+
# `search` here will be whatever `search_first_name` returned.
|
61
66
|
search.where(last_name: last_name)
|
62
67
|
end
|
63
68
|
end
|
64
69
|
```
|
65
70
|
|
66
|
-
|
71
|
+
Calling `PersonSearch.new("first_name" => "Gregor", "last_name" => "Mendel").results` would run `Person.all.where(first_name: "Gregor").where(last_name: "Mendel")` and return the resulting `ActiveRecord::Relation`. If you omitted the `last_name` option, or provided `"last_name" => ""`, the second `where` would not be added.
|
72
|
+
|
73
|
+
Here's a fuller example search class. Note that **because Searchlight doesn't write queries for you, you're free to do anything your ORM supports**. (See `spec/support/book_search.rb` for even more fanciness.)
|
67
74
|
|
68
75
|
```ruby
|
69
76
|
# app/searches/city_search.rb
|
70
77
|
class CitySearch < Searchlight::Search
|
71
78
|
|
72
|
-
# `City` here is an ActiveRecord model
|
73
|
-
|
74
|
-
|
75
|
-
|
79
|
+
# `City` here is an ActiveRecord model
|
80
|
+
def base_query
|
81
|
+
City.includes(:country)
|
82
|
+
end
|
76
83
|
|
77
84
|
# Reach into other tables
|
78
85
|
def search_continent
|
@@ -84,10 +91,9 @@ class CitySearch < Searchlight::Search
|
|
84
91
|
search.where("`countries`.`name` LIKE ?", "%#{country_name_like}%")
|
85
92
|
end
|
86
93
|
|
87
|
-
#
|
88
|
-
# considering 'false', 0, and '0' to be false
|
94
|
+
# .checked? considers "false", 0 and "0" to be false
|
89
95
|
def search_is_megacity
|
90
|
-
search.where("`cities`.`population` #{is_megacity
|
96
|
+
search.where("`cities`.`population` #{checked?(is_megacity) ? '>=' : '<'} ?", 10_000_000)
|
91
97
|
end
|
92
98
|
|
93
99
|
end
|
@@ -98,12 +104,12 @@ Here are some example searches.
|
|
98
104
|
```ruby
|
99
105
|
CitySearch.new.results.to_sql
|
100
106
|
# => "SELECT `cities`.* FROM `cities` "
|
101
|
-
CitySearch.new(name
|
107
|
+
CitySearch.new("name" => "Nairobi").results.to_sql
|
102
108
|
# => "SELECT `cities`.* FROM `cities` WHERE `cities`.`name` = 'Nairobi'"
|
103
109
|
|
104
|
-
CitySearch.new(country_name_like
|
110
|
+
CitySearch.new("country_name_like" => "aust", "continent" => "Europe").results.count # => 6
|
105
111
|
|
106
|
-
non_megas = CitySearch.new(is_megacity
|
112
|
+
non_megas = CitySearch.new("is_megacity" => "false")
|
107
113
|
non_megas.results.to_sql
|
108
114
|
# => "SELECT `cities`.* FROM `cities` WHERE (`cities`.`population` < 100000"
|
109
115
|
non_megas.results.each do |city|
|
@@ -113,152 +119,105 @@ end
|
|
113
119
|
|
114
120
|
### Accessors
|
115
121
|
|
116
|
-
For each search
|
122
|
+
For each search method you define, Searchlight will define a corresponding accessor method. Eg, if you add `def search_first_name`, your search class will get a `.first_name` method that returns `options["first_name"]`. This is useful mainly when building forms.
|
117
123
|
|
118
|
-
|
124
|
+
**Note that this checks for string keys** - you must either use them, or use something like `ActiveSupport::HashWithIndifferentAccess`.
|
119
125
|
|
120
|
-
|
121
|
-
search = MySearchClass.new(awesomeness: 'Xtreme')
|
122
|
-
```
|
126
|
+
### Examining Options
|
123
127
|
|
124
|
-
|
128
|
+
Searchlight provides some methods for examining the options provided to your search.
|
125
129
|
|
126
|
-
- `
|
127
|
-
- `
|
130
|
+
- `raw_options` contains exactly what it was instantiated with
|
131
|
+
- `options` contains all `raw_options` that weren't `empty?`. Eg, if `raw_options` is `categories: nil, tags: ["a", ""]`, options will be `tags: ["a"]`.
|
132
|
+
- `empty?(value)` returns true for `nil`, whitespace-only strings, or anything else that returns true from `value.empty?` (eg, empty arrays)
|
133
|
+
- `checked?(value)` returns a boolean, which mostly works like `!!value` but considers `0`, `"0"`, and `"false"` to be `false`
|
128
134
|
|
129
|
-
|
135
|
+
Finally, `explain` will tell you how Searchlight interpreted your options. Eg, `book_search.explain` might output:
|
130
136
|
|
131
|
-
|
137
|
+
```
|
138
|
+
Initialized with `raw_options`: ["title_like", "author_name_like", "category_in",
|
139
|
+
"tags", "book_thickness", "parts_about_lolcats"]
|
132
140
|
|
133
|
-
|
134
|
-
|
141
|
+
Of those, the non-blank ones are available as `options`: ["title_like",
|
142
|
+
"author_name_like", "tags", "book_thickness", "in_print"]
|
135
143
|
|
136
|
-
|
144
|
+
Of those, the following have corresponding `search_` methods: ["title_like",
|
145
|
+
"author_name_like", "in_print"]. These would be used to build the query.
|
137
146
|
|
138
|
-
|
139
|
-
# Make sure this is an array and never search for Jimmy.
|
140
|
-
# Jimmy is a private man. An old-fashioned man. Leave him be.
|
141
|
-
Array(super).reject { |name| name == 'Jimmy' }
|
142
|
-
end
|
147
|
+
Blank options are: ["category_in", "parts_about_lolcats"]
|
143
148
|
|
144
|
-
|
145
|
-
|
146
|
-
end
|
147
|
-
|
148
|
-
def awesomeness?
|
149
|
-
# Disagree about what is awesome
|
150
|
-
!super
|
151
|
-
end
|
152
|
-
|
153
|
-
end
|
149
|
+
Non-blank options with no corresponding `search_` method are: ["tags",
|
150
|
+
"book_thickness"]
|
154
151
|
```
|
155
152
|
|
156
|
-
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.
|
157
|
-
|
158
153
|
### Defining Defaults
|
159
154
|
|
160
|
-
|
155
|
+
Sometimes it's useful to have default search options - eg, "orders that haven't been fulfilled" or "houses listed in the last month".
|
161
156
|
|
162
|
-
|
157
|
+
This can be done by overriding `options`. Eg:
|
163
158
|
|
164
|
-
|
159
|
+
```ruby
|
160
|
+
class BookSearch < SearchlightSearch
|
165
161
|
|
166
|
-
|
162
|
+
# def base_query...
|
167
163
|
|
168
|
-
def
|
169
|
-
super
|
170
|
-
|
164
|
+
def options
|
165
|
+
super.tap { |opts|
|
166
|
+
opts["in_print"] ||= "either"
|
167
|
+
}
|
171
168
|
end
|
172
169
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
CitySearch.new.results.to_sql
|
177
|
-
=> "SELECT `cities`.* FROM `cities` WHERE (`countries`.`continent` = 'Asia')"
|
178
|
-
CitySearch.new(continent: 'Europe').results.to_sql
|
179
|
-
=> "SELECT `cities`.* FROM `cities` WHERE (`countries`.`continent` = 'Europe')"
|
180
|
-
```
|
181
|
-
|
182
|
-
You can define defaults for boolean attributes if you treat them as "yes/no/either" choices.
|
183
|
-
|
184
|
-
```ruby
|
185
|
-
class AnimalSearch < Searchlight::Search
|
186
|
-
|
187
|
-
search_on Animal.all
|
188
|
-
|
189
|
-
searches :is_fictional
|
190
|
-
|
191
|
-
def initialize(*args)
|
192
|
-
super
|
193
|
-
self.is_fictional = :either if is_fictional.blank?
|
194
|
-
end
|
195
|
-
|
196
|
-
def search_is_fictional
|
197
|
-
case is_fictional.to_s
|
198
|
-
when 'true' then search.where(fictional: true)
|
199
|
-
when 'false' then search.where(fictional: false)
|
200
|
-
when 'either' then search # unmodified
|
201
|
-
end
|
170
|
+
def search_in_print
|
171
|
+
return query if options["in_print"].to_s == "either"
|
172
|
+
query.where(in_print: checked?(options["in_print"]))
|
202
173
|
end
|
203
|
-
end
|
204
|
-
|
205
174
|
|
206
|
-
|
207
|
-
=> "SELECT `animals`.* FROM `animals` WHERE (`fictional` = true)"
|
208
|
-
AnimalSearch.new(fictional: false).results.to_sql
|
209
|
-
=> "SELECT `animals`.* FROM `animals` WHERE (`fictional` = false)"
|
210
|
-
AnimalSearch.new.results.to_sql
|
211
|
-
=> "SELECT `animals`.* FROM `animals`"
|
175
|
+
end
|
212
176
|
```
|
213
177
|
|
214
178
|
### Subclassing
|
215
179
|
|
216
|
-
You can subclass an existing search class and support all the same options with a different
|
180
|
+
You can subclass an existing search class and support all the same options with a different base query. This may be useful for single table inheritance, for example.
|
217
181
|
|
218
182
|
```ruby
|
219
183
|
class VillageSearch < CitySearch
|
220
|
-
|
184
|
+
def base_query
|
185
|
+
Village.all
|
186
|
+
end
|
221
187
|
end
|
222
188
|
```
|
223
189
|
|
224
|
-
|
190
|
+
Or you can use `super` to get the superclass's `base_query` value and modify it:
|
225
191
|
|
226
192
|
```ruby
|
227
193
|
class SmallTownSearch < CitySearch
|
228
|
-
|
194
|
+
def base_query
|
195
|
+
super.where("`cities`.`population` < ?", 1_000)
|
196
|
+
end
|
229
197
|
end
|
230
|
-
|
231
|
-
SmallTownSearch.new(country_name_like: 'Norfolk').results.to_sql
|
232
|
-
=> "SELECT `cities`.* FROM `cities` WHERE (`cities`.`population` < 1000) AND (`countries`.`name` LIKE '%Norfolk%')"
|
233
198
|
```
|
234
199
|
|
235
|
-
###
|
200
|
+
### Custom Options
|
236
201
|
|
237
|
-
|
202
|
+
You can provide a Searchlight search any options you like; only those with a matching `search_` method will determine what methods are run. Eg, if you want to do `AccountSearch.new("super_user" => true)` to find restricted results, just ensure that you check `options["super_user"]` when building your query.
|
238
203
|
|
239
|
-
|
240
|
-
class RecentOrdersSearch < Searchlight::Search
|
241
|
-
search_on proc { Orders.since(Time.now - 3.hours) }
|
242
|
-
end
|
243
|
-
```
|
204
|
+
## Usage in Rails
|
244
205
|
|
245
|
-
|
206
|
+
### ActionView adapter
|
246
207
|
|
247
|
-
|
248
|
-
class ExpensiveRecentOrdersSearch < RecentOrderSearch
|
249
|
-
search_on proc { superclass.search_target.call.expensive }
|
250
|
-
end
|
251
|
-
```
|
208
|
+
Searchlight plays nicely with Rails forms - just include the `ActionView` adapter as follows:
|
252
209
|
|
253
|
-
|
254
|
-
|
255
|
-
To allow search options that don't trigger searches directly, just use `attr_accessor`.
|
210
|
+
```ruby
|
211
|
+
require "searchlight/adapters/action_view"
|
256
212
|
|
257
|
-
|
213
|
+
class MySearch < Searchlight::Search
|
214
|
+
include Searchlight::Adapters::ActionView
|
258
215
|
|
259
|
-
|
216
|
+
# ...etc
|
217
|
+
end
|
218
|
+
```
|
260
219
|
|
261
|
-
|
220
|
+
This will enable using a `Searchlight::Search` with `form_for`:
|
262
221
|
|
263
222
|
```ruby
|
264
223
|
# app/views/cities/index.html.haml
|
@@ -283,7 +242,7 @@ Searchlight plays nicely with Rails forms. All search options and any `attr_acce
|
|
283
242
|
= f.submit "Search"
|
284
243
|
|
285
244
|
- @results.each do |city|
|
286
|
-
= render 'city'
|
245
|
+
= render partial: 'city', locals: {city: city}
|
287
246
|
```
|
288
247
|
|
289
248
|
### Controllers
|
@@ -307,9 +266,6 @@ class OrdersController < ApplicationController
|
|
307
266
|
end
|
308
267
|
end
|
309
268
|
```
|
310
|
-
## ActionView Adapter
|
311
|
-
|
312
|
-
Searchlight's ActionView adapter adds ActionView-friendly methods to your classes if it sees that `ActionView` is a defined constant. See the code for details, but the upshot is that you can use a search with `form_for`.
|
313
269
|
|
314
270
|
## Compatibility
|
315
271
|
|
data/Rakefile
CHANGED
data/TODO.md
CHANGED
data/lib/searchlight.rb
CHANGED
@@ -1,12 +1,7 @@
|
|
1
|
-
require 'named'
|
2
|
-
require 'searchlight/version'
|
3
|
-
|
4
1
|
module Searchlight
|
5
|
-
Error = Class.new(StandardError)
|
6
|
-
end
|
7
2
|
|
8
|
-
require 'searchlight/dsl'
|
9
|
-
require 'searchlight/search'
|
10
|
-
if defined?(::ActionView) && defined?(::ActiveModel)
|
11
|
-
require 'searchlight/adapters/action_view'
|
12
3
|
end
|
4
|
+
|
5
|
+
require "searchlight/version"
|
6
|
+
|
7
|
+
require "searchlight/search"
|
@@ -8,8 +8,8 @@ module Searchlight
|
|
8
8
|
|
9
9
|
include ::ActiveModel::Conversion
|
10
10
|
|
11
|
-
def self.included(
|
12
|
-
|
11
|
+
def self.included(search_class)
|
12
|
+
search_class.extend ::ActiveModel::Naming
|
13
13
|
end
|
14
14
|
|
15
15
|
def persisted?
|
@@ -19,5 +19,3 @@ module Searchlight
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
end
|
22
|
-
|
23
|
-
Searchlight::Search.send(:include, Searchlight::Adapters::ActionView)
|