searchlight 3.1.1 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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)
|