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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2417c0b1b3d3b3a7de838a2c5129c3182c421083
4
- data.tar.gz: fdb71cd159542903b8a21065962564fec9d9f14d
3
+ metadata.gz: ad62f9410f019b9c6bb09ce5e1e6b1e804ee152c
4
+ data.tar.gz: 154208ad780633fff9670fd0a174ae3d46ae228a
5
5
  SHA512:
6
- metadata.gz: f8da4a68ca0e200a61dcd3fcaaba0eb44b2e549b203511a1408d3cab0aa71e502797ecb7f5cb644e2dfd955cbdc6ce838101066328cd9a3dbe0bdf9df6202692
7
- data.tar.gz: a8c3067c059af9749675bdbae399b06073e9e94eaa5e58af233856f554bcc9c1634dd4400e206f46340fc4fc1054026311cc1f60bfd3678b4b1274744beafc3f
6
+ metadata.gz: 0aba3b57deac310d31274584c06ecd8075c2c709e99b8ca2190dd7fffadee25cd31a78b162ca750c5b3ea054d134b61e385b8d7f0eacd12da07f039c530dbcfd
7
+ data.tar.gz: dfb54abe4eadcb4a3657a37225315ed0e8c1c79d65e035b5655f306fb329b2e9190529f50e2ca6fd3c98dd9f4b23bc2fe4c0ab91316708e174f6e8c92e012a66
data/.travis.yml CHANGED
@@ -1,5 +1,4 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.9.3
4
3
  - 2.0.0
5
- - 2.1.1
4
+ - 2.2.3
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
 
@@ -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
- The basic idea of Searchlight is to build a search by chaining method calls that you define. It calls **public** methods on the object you specify, based on the options you pass.
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
- active: true, name: 'Jimmy', location_in: %w[NY LA] # or params[:yeti_search]
30
+ # or params[:yeti_search]
31
+ "active" => true, "name" => "Jimmy", "location_in" => %w[NY LA]
25
32
  )
26
33
  ```
27
34
 
28
- ... calling `results` on the instance will build a search by chaining calls to `search_active`, `search_name`, and `search_location_in`.
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 three main parts: a target, options, and methods. For example:
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
- search_on Person.all
45
-
46
- # The options the search understands. Supply any combination of them to an instance.
47
- searches :first_name, :last_name
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 this is the first search method called, `search` here will be
52
- # the search target, namely, `Person`.
53
- # `first_name` is an automatically-defined accessor for the option value.
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 this is the second search method called, `search` here will be
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
- Here's a fuller example search class.
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 (see notes below on the adapter)
73
- search_on City.includes(:country)
74
-
75
- searches :name, :continent, :country_name_like, :is_megacity
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
- # For every option, we also add an accessor that coerces to a boolean,
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? ? '>=' : '<'} ?", 10_000_000)
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: 'Nairobi').results.to_sql
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: 'aust', continent: 'Europe').results.count # => 6
110
+ CitySearch.new("country_name_like" => "aust", "continent" => "Europe").results.count # => 6
105
111
 
106
- non_megas = CitySearch.new(is_megacity: 'false')
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 option you allow, Searchlight defines two accessors: one for a value, and one for a boolean.
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
- For example, if your class `searches :awesomeness` and gets instantiated like:
124
+ **Note that this checks for string keys** - you must either use them, or use something like `ActiveSupport::HashWithIndifferentAccess`.
119
125
 
120
- ```ruby
121
- search = MySearchClass.new(awesomeness: 'Xtreme')
122
- ```
126
+ ### Examining Options
123
127
 
124
- ... your search methods can use:
128
+ Searchlight provides some methods for examining the options provided to your search.
125
129
 
126
- - `awesomeness` to retrieve the given value, `'Xtreme'`
127
- - `awesomeness?` to get a boolean version: `true`
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
- The boolean conversion is form-friendly, so that `0`, `'0'`, and `'false'` are considered `false`.
135
+ Finally, `explain` will tell you how Searchlight interpreted your options. Eg, `book_search.explain` might output:
130
136
 
131
- All accessors are defined in modules, so you can override them and use `super` to call the original methods.
137
+ ```
138
+ Initialized with `raw_options`: ["title_like", "author_name_like", "category_in",
139
+ "tags", "book_thickness", "parts_about_lolcats"]
132
140
 
133
- ```ruby
134
- class PersonSearch < Searchlight::Search
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
- searches :names, :awesomeness
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
- def names
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
- def searches_names
145
- search.where("name IN (?)", names)
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
- Set defaults using plain Ruby. These can be used to prefill a form or to assume what the user didn't specify.
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
- ```ruby
157
+ This can be done by overriding `options`. Eg:
163
158
 
164
- class CitySearch < Searchlight::Search
159
+ ```ruby
160
+ class BookSearch < SearchlightSearch
165
161
 
166
- #...
162
+ # def base_query...
167
163
 
168
- def initialize(options = {})
169
- super
170
- self.continent ||= 'Asia'
164
+ def options
165
+ super.tap { |opts|
166
+ opts["in_print"] ||= "either"
167
+ }
171
168
  end
172
169
 
173
- #...
174
- end
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
- AnimalSearch.new(fictional: true).results.to_sql
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 search target. This may be useful for single table inheritance, for example.
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
- search_on Village.all
184
+ def base_query
185
+ Village.all
186
+ end
221
187
  end
222
188
  ```
223
189
 
224
- You can also use `search_target` to get the superclass's `search_on` value, so you can do this:
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
- search_on search_target.where("`cities`.`population` < ?", 1_000)
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
- ### Delayed scope evaluation
200
+ ### Custom Options
236
201
 
237
- If your search target has a time-sensitive condition, you can wrap it in a callable object to prevent it from being evaluated when the class is defined. For example:
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
- ```ruby
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
- This does make subclassing a bit more complex:
206
+ ### ActionView adapter
246
207
 
247
- ```ruby
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
- ### Dependent Options
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
- ## Usage in Rails
213
+ class MySearch < Searchlight::Search
214
+ include Searchlight::Adapters::ActionView
258
215
 
259
- ### Forms
216
+ # ...etc
217
+ end
218
+ ```
260
219
 
261
- Searchlight plays nicely with Rails forms. All search options and any `attr_accessor`s you define can be hooked up to form fields.
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
@@ -5,3 +5,11 @@ require 'rspec/core/rake_task'
5
5
  RSpec::Core::RakeTask.new(:spec)
6
6
 
7
7
  task "default" => "spec"
8
+
9
+ task :console do
10
+ require 'irb'
11
+ require 'irb/completion'
12
+ require 'searchlight'
13
+ ARGV.clear
14
+ IRB.start
15
+ end
data/TODO.md CHANGED
@@ -1,4 +1,5 @@
1
1
  # TODO
2
2
 
3
3
  - Do some mutation testing
4
- - Make nice Github page
4
+ - Make nice Github page - logo?
5
+ - Mention to micro gems site, maybe people like Piotr Solnica and Nick Sutterer
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(target)
12
- target.extend ::ActiveModel::Naming
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)