searchlight 4.0.0 → 4.1.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/CHANGELOG.md +4 -1
- data/README.md +12 -10
- data/Rakefile +17 -0
- data/lib/searchlight/options.rb +6 -6
- data/lib/searchlight/search.rb +7 -2
- data/lib/searchlight/version.rb +1 -1
- data/spec/searchlight/options_spec.rb +10 -4
- data/spec/searchlight/search_spec.rb +41 -1
- data/spec/spec_helper.rb +1 -0
- data/spec/support/fancy_hash.rb +3 -0
- data/spec/support/mock_model.rb +7 -11
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: be95aae20e317b4b6be6f0124ab80790405b62e5
|
4
|
+
data.tar.gz: 370872f18bf03156a833b49052fd034019b37bf1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fddc94f601422650e35d6626041b454adb226163dbd5d9685b6864415fc408ca8cd5d918cc52fe72a37e56fe4f12a57e771e73e24a957c0b74a6b286e0cf19b0
|
7
|
+
data.tar.gz: 397ac3fc0b5b8c249eff00f332c508fb23fef38c9576f21870a01864d91d8a79433d35e34389bd9f15dea928e786ecef88e33e0f2ce77a44ed5b5e365d189f0c
|
data/CHANGELOG.md
CHANGED
@@ -4,7 +4,9 @@ Searchlight does its best to use [semantic versioning](http://semver.org), for t
|
|
4
4
|
|
5
5
|
## Unreleased
|
6
6
|
|
7
|
-
|
7
|
+
## v4.1.0 - 2015-10-29
|
8
|
+
|
9
|
+
- Option readers now will find symbol keys as well as string keys
|
8
10
|
|
9
11
|
## v4.0.0 - 2015-10-28
|
10
12
|
|
@@ -18,6 +20,7 @@ Removed the DSL methods to simplify some things. This is a breaking change, but
|
|
18
20
|
- `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
21
|
- `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
22
|
- `Searchlight::Adapters::ActionView` adapter must now be explicitly required and included.
|
23
|
+
- Within your `search_` methods, the query you chain on is now called `query`, not `search`
|
21
24
|
|
22
25
|
## v3.1.1
|
23
26
|
|
data/README.md
CHANGED
@@ -11,7 +11,7 @@ Searchlight can work with **any** ORM or object that can build a query using **c
|
|
11
11
|
|
12
12
|
## Getting Started
|
13
13
|
|
14
|
-
|
14
|
+
A [demo app](http://bookfinder-searchlight-demo.herokuapp.com) and [the code for that app](https://github.com/nathanl/searchlight_demo) are available to help you get started.
|
15
15
|
|
16
16
|
## Overview
|
17
17
|
|
@@ -55,15 +55,15 @@ class PersonSearch < Searchlight::Search
|
|
55
55
|
# A search method.
|
56
56
|
def search_first_name
|
57
57
|
# If `"first_name"` was the first key in the options_hash,
|
58
|
-
# `
|
59
|
-
|
58
|
+
# `query` here will be the base query, namely, `Person.all`.
|
59
|
+
query.where(first_name: options[:first_name])
|
60
60
|
end
|
61
61
|
|
62
62
|
# Another search method.
|
63
63
|
def search_last_name
|
64
64
|
# If `"last_name"` was the second key in the options_hash,
|
65
|
-
# `
|
66
|
-
|
65
|
+
# `query` here will be whatever `search_first_name` returned.
|
66
|
+
query.where(last_name: last_name)
|
67
67
|
end
|
68
68
|
end
|
69
69
|
```
|
@@ -83,17 +83,17 @@ class CitySearch < Searchlight::Search
|
|
83
83
|
|
84
84
|
# Reach into other tables
|
85
85
|
def search_continent
|
86
|
-
|
86
|
+
query.where('`countries`.`continent` = ?', continent)
|
87
87
|
end
|
88
88
|
|
89
89
|
# Other kinds of queries
|
90
90
|
def search_country_name_like
|
91
|
-
|
91
|
+
query.where("`countries`.`name` LIKE ?", "%#{country_name_like}%")
|
92
92
|
end
|
93
93
|
|
94
94
|
# .checked? considers "false", 0 and "0" to be false
|
95
95
|
def search_is_megacity
|
96
|
-
|
96
|
+
query.where("`cities`.`population` #{checked?(is_megacity) ? '>=' : '<'} ?", 10_000_000)
|
97
97
|
end
|
98
98
|
|
99
99
|
end
|
@@ -117,9 +117,9 @@ non_megas.results.each do |city|
|
|
117
117
|
end
|
118
118
|
```
|
119
119
|
|
120
|
-
###
|
120
|
+
### Option Readers
|
121
121
|
|
122
|
-
For each search method you define, Searchlight will define a corresponding
|
122
|
+
For each search method you define, Searchlight will define a corresponding option reader 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.
|
123
123
|
|
124
124
|
**Note that this checks for string keys** - you must either use them, or use something like `ActiveSupport::HashWithIndifferentAccess`.
|
125
125
|
|
@@ -287,6 +287,8 @@ Or install it yourself as:
|
|
287
287
|
|
288
288
|
## Contributing
|
289
289
|
|
290
|
+
`rake` runs the tests; `rake mutant` runs mutation tests using [mutant](https://github.com/mbj/mutant).
|
291
|
+
|
290
292
|
1. Fork it
|
291
293
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
292
294
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
data/Rakefile
CHANGED
@@ -13,3 +13,20 @@ task :console do
|
|
13
13
|
ARGV.clear
|
14
14
|
IRB.start
|
15
15
|
end
|
16
|
+
|
17
|
+
desc "Mutation test with mutant gem. Provide scope, eg: Searchlight::Search or Searchlight::Options#excluding_empties"
|
18
|
+
task :mutant do
|
19
|
+
scope = ENV.fetch("SCOPE") {
|
20
|
+
puts "Must set SCOPE env variable, eg `SCOPE=Searchlight::Search`"
|
21
|
+
exit
|
22
|
+
}
|
23
|
+
|
24
|
+
ARGV.clear
|
25
|
+
command = "mutant --include lib --require searchlight --use rspec #{scope}"
|
26
|
+
begin
|
27
|
+
exec(command)
|
28
|
+
rescue Errno::ENOENT
|
29
|
+
puts "Could not find mutant executable - please install gem 'mutant-rspec'"
|
30
|
+
puts "(Not included as a test dependency because it breaks CI; mutant only works with Ruby > 2.1.0"
|
31
|
+
end
|
32
|
+
end
|
data/lib/searchlight/options.rb
CHANGED
@@ -3,7 +3,7 @@ module Searchlight::Options
|
|
3
3
|
def self.empty?(value)
|
4
4
|
return true if value.nil?
|
5
5
|
return true if value.respond_to?(:empty?) && value.empty?
|
6
|
-
return true if
|
6
|
+
return true if /\A[[:space:]]*\z/ === value
|
7
7
|
false
|
8
8
|
end
|
9
9
|
|
@@ -14,14 +14,14 @@ module Searchlight::Options
|
|
14
14
|
def self.excluding_empties(input)
|
15
15
|
output = input.dup
|
16
16
|
output.each do |key, value|
|
17
|
-
if value.is_a?(Array)
|
18
|
-
output[key] = value.reject { |v| empty?(v) }
|
19
|
-
end
|
20
17
|
if value.is_a?(Hash)
|
21
|
-
output[key] = value.reject { |
|
18
|
+
output[key] = value.reject { |_, v| empty?(v) }
|
19
|
+
end
|
20
|
+
if value.instance_of?(Array)
|
21
|
+
output[key] = value.reject { |v| empty?(v) }
|
22
22
|
end
|
23
23
|
end
|
24
|
-
output.reject { |
|
24
|
+
output.reject { |_, value| empty?(value) }
|
25
25
|
end
|
26
26
|
|
27
27
|
end
|
data/lib/searchlight/search.rb
CHANGED
@@ -12,12 +12,17 @@ class Searchlight::Search
|
|
12
12
|
option_name = match.captures.fetch(0)
|
13
13
|
# accessor - eg, if method_name is #search_title, define #title
|
14
14
|
define_method(option_name) do
|
15
|
-
options[option_name]
|
15
|
+
options.key?(option_name) ? options[option_name] : options[option_name.to_sym]
|
16
16
|
end
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
20
|
def initialize(raw_options = {})
|
21
|
+
string_keys, non_string_keys = raw_options.keys.partition {|k| k.is_a?(String) }
|
22
|
+
intersection = string_keys & non_string_keys.map(&:to_s)
|
23
|
+
if intersection.any?
|
24
|
+
fail ArgumentError, "more than one key converts to these string values: #{intersection}"
|
25
|
+
end
|
21
26
|
@raw_options = raw_options
|
22
27
|
end
|
23
28
|
|
@@ -49,7 +54,7 @@ class Searchlight::Search
|
|
49
54
|
|
50
55
|
def options_with_search_methods
|
51
56
|
{}.tap do |map|
|
52
|
-
options.each do |option_name,
|
57
|
+
options.each do |option_name, _|
|
53
58
|
method_name = "search_#{option_name}"
|
54
59
|
map[option_name] = method_name if respond_to?(method_name)
|
55
60
|
end
|
data/lib/searchlight/version.rb
CHANGED
@@ -52,6 +52,11 @@ describe Searchlight::Options do
|
|
52
52
|
describe "excluding empties" do
|
53
53
|
|
54
54
|
it "removes empty values at the top level of the hash" do
|
55
|
+
# Simulate something like HashWithIndifferentAccess
|
56
|
+
relations = FancyHash.new
|
57
|
+
relations[:uncle] = "Jimmy"
|
58
|
+
relations[:aunt] = nil
|
59
|
+
|
55
60
|
expect(
|
56
61
|
mod.excluding_empties(
|
57
62
|
name: "Bob",
|
@@ -59,8 +64,8 @@ describe Searchlight::Options do
|
|
59
64
|
likes: ["pizza", "fish"],
|
60
65
|
dislikes: [],
|
61
66
|
elvish: false,
|
62
|
-
relations:
|
63
|
-
|
67
|
+
relations: relations,
|
68
|
+
enemies: {},
|
64
69
|
)
|
65
70
|
).to eq(
|
66
71
|
name: "Bob",
|
@@ -75,10 +80,11 @@ describe Searchlight::Options do
|
|
75
80
|
it "does not remove empty values from more deeply-nested elements" do
|
76
81
|
expect(
|
77
82
|
mod.excluding_empties(
|
78
|
-
tags: ["one", "two", "", nil, "three", ["a", "", nil, "b"], {a: ""}]
|
83
|
+
tags: ["one", "two", "", nil, "three", ["a", "", nil, "b"], {a: ""}],
|
84
|
+
|
79
85
|
)
|
80
86
|
).to eq(
|
81
|
-
tags: ["one", "two", "three", ["a", "", nil, "b"], {a: ""}]
|
87
|
+
tags: ["one", "two", "three", ["a", "", nil, "b"], {a: ""}],
|
82
88
|
)
|
83
89
|
end
|
84
90
|
|
@@ -14,6 +14,22 @@ describe Searchlight::Search do
|
|
14
14
|
}
|
15
15
|
let(:search) { BookSearch.new(raw_options) }
|
16
16
|
|
17
|
+
describe "initialization" do
|
18
|
+
|
19
|
+
it "doesn't require options" do
|
20
|
+
expect(BookSearch.new.results).to eq(BookSearch.new({}).results)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "blows up if there is a string/symbol key conflict" do
|
24
|
+
expect {
|
25
|
+
described_class.new(a: 1, "a" => 2)
|
26
|
+
}.to raise_error(
|
27
|
+
ArgumentError, %Q{more than one key converts to these string values: ["a"]}
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
17
33
|
describe "parsing options" do
|
18
34
|
|
19
35
|
it "makes the raw options available" do
|
@@ -39,6 +55,18 @@ describe Searchlight::Search do
|
|
39
55
|
end
|
40
56
|
end
|
41
57
|
|
58
|
+
describe "option readers" do
|
59
|
+
|
60
|
+
it "has an option reader method for each search method, which can read strings or symbols" do
|
61
|
+
expect(search.author_name_like).to eq("Lew")
|
62
|
+
expect(search.title_like).to eq("Mere Christianity")
|
63
|
+
expect(search.board_book).to eq(nil)
|
64
|
+
expect{search.book_thickness}.to raise_error(NoMethodError)
|
65
|
+
expect{search.not_an_option}.to raise_error(NoMethodError)
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
42
70
|
describe "querying" do
|
43
71
|
|
44
72
|
it "builds results by running all methods matching its options" do
|
@@ -55,7 +83,19 @@ describe Searchlight::Search do
|
|
55
83
|
end
|
56
84
|
|
57
85
|
it "has an 'explain' method to show how it builds its query" do
|
58
|
-
expect(search.explain).to
|
86
|
+
expect(search.explain).to eq(
|
87
|
+
%Q{
|
88
|
+
Initialized with `raw_options`: [:title_like, "author_name_like", :category_in, :tags, :book_thickness, :parts_about_lolcats]
|
89
|
+
|
90
|
+
Of those, the non-blank ones are available as `options`: [:title_like, "author_name_like", :tags, :book_thickness, :in_print]
|
91
|
+
|
92
|
+
Of those, the following have corresponding `search_` methods: [:title_like, "author_name_like", :in_print]. These would be used to build the query.
|
93
|
+
|
94
|
+
Blank options are: [:category_in, :parts_about_lolcats]
|
95
|
+
|
96
|
+
Non-blank options with no corresponding `search_` method are: [:tags, :book_thickness]
|
97
|
+
}.strip
|
98
|
+
)
|
59
99
|
end
|
60
100
|
|
61
101
|
end
|
data/spec/spec_helper.rb
CHANGED
data/spec/support/mock_model.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
class MockModel
|
2
2
|
|
3
3
|
def self.method_missing(method, *args, &block)
|
4
|
-
MockRelation.new(method)
|
4
|
+
MockRelation.new([method])
|
5
5
|
end
|
6
6
|
|
7
7
|
end
|
@@ -9,20 +9,16 @@ end
|
|
9
9
|
class MockRelation
|
10
10
|
attr_reader :called_methods
|
11
11
|
|
12
|
-
def initialize(
|
13
|
-
@called_methods =
|
12
|
+
def initialize(called_methods)
|
13
|
+
@called_methods = called_methods
|
14
14
|
end
|
15
15
|
|
16
16
|
def method_missing(method, *args, &block)
|
17
|
-
|
17
|
+
self.class.new(called_methods + [method])
|
18
18
|
end
|
19
|
-
|
20
|
-
def
|
21
|
-
|
22
|
-
end
|
23
|
-
|
24
|
-
def engine
|
25
|
-
MockActiveRecord
|
19
|
+
|
20
|
+
def ==(other)
|
21
|
+
other.class == self.class && other.called_methods == called_methods
|
26
22
|
end
|
27
23
|
|
28
24
|
end
|
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: 4.
|
4
|
+
version: 4.1.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: 2015-10-
|
12
|
+
date: 2015-10-29 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
@@ -129,6 +129,7 @@ files:
|
|
129
129
|
- spec/searchlight_spec.rb
|
130
130
|
- spec/spec_helper.rb
|
131
131
|
- spec/support/book_search.rb
|
132
|
+
- spec/support/fancy_hash.rb
|
132
133
|
- spec/support/mock_model.rb
|
133
134
|
homepage: https://github.com/nathanl/searchlight
|
134
135
|
licenses:
|
@@ -161,4 +162,5 @@ test_files:
|
|
161
162
|
- spec/searchlight_spec.rb
|
162
163
|
- spec/spec_helper.rb
|
163
164
|
- spec/support/book_search.rb
|
165
|
+
- spec/support/fancy_hash.rb
|
164
166
|
- spec/support/mock_model.rb
|