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