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 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)