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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ad62f9410f019b9c6bb09ce5e1e6b1e804ee152c
4
- data.tar.gz: 154208ad780633fff9670fd0a174ae3d46ae228a
3
+ metadata.gz: be95aae20e317b4b6be6f0124ab80790405b62e5
4
+ data.tar.gz: 370872f18bf03156a833b49052fd034019b37bf1
5
5
  SHA512:
6
- metadata.gz: 0aba3b57deac310d31274584c06ecd8075c2c709e99b8ca2190dd7fffadee25cd31a78b162ca750c5b3ea054d134b61e385b8d7f0eacd12da07f039c530dbcfd
7
- data.tar.gz: dfb54abe4eadcb4a3657a37225315ed0e8c1c79d65e035b5655f306fb329b2e9190529f50e2ca6fd3c98dd9f4b23bc2fe4c0ab91316708e174f6e8c92e012a66
6
+ metadata.gz: fddc94f601422650e35d6626041b454adb226163dbd5d9685b6864415fc408ca8cd5d918cc52fe72a37e56fe4f12a57e771e73e24a957c0b74a6b286e0cf19b0
7
+ data.tar.gz: 397ac3fc0b5b8c249eff00f332c508fb23fef38c9576f21870a01864d91d8a79433d35e34389bd9f15dea928e786ecef88e33e0f2ce77a44ed5b5e365d189f0c
@@ -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
- Nothing
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
- An [introductory video](https://vimeo.com/69179161), [the demo app it uses](http://bookfinder-searchlight-demo.herokuapp.com) and [the code for that app](https://github.com/nathanl/bookfinder) are available to help you get started.
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
- # `search` here will be the base query, namely, `Person.all`.
59
- search.where(first_name: options[:first_name])
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
- # `search` here will be whatever `search_first_name` returned.
66
- search.where(last_name: last_name)
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
- search.where('`countries`.`continent` = ?', continent)
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
- search.where("`countries`.`name` LIKE ?", "%#{country_name_like}%")
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
- search.where("`cities`.`population` #{checked?(is_megacity) ? '>=' : '<'} ?", 10_000_000)
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
- ### Accessors
120
+ ### Option Readers
121
121
 
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.
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
@@ -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 value.is_a?(String) && /\A[[:space:]]*\z/ === value
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 { |k, v| empty?(v) }
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 { |key, value| empty?(value) }
24
+ output.reject { |_, value| empty?(value) }
25
25
  end
26
26
 
27
27
  end
@@ -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, option_value|
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
@@ -1,3 +1,3 @@
1
1
  module Searchlight
2
- VERSION = "4.0.0"
2
+ VERSION = "4.1.0"
3
3
  end
@@ -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: {uncle: "Jimmy"},
63
- eh: {},
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 be_a(String)
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
@@ -3,6 +3,7 @@ require 'searchlight'
3
3
  $LOAD_PATH << '.'
4
4
  require 'support/mock_model'
5
5
  require 'support/book_search'
6
+ require 'support/fancy_hash'
6
7
 
7
8
  RSpec.configure do |config|
8
9
  config.run_all_when_everything_filtered = true
@@ -0,0 +1,3 @@
1
+ # Eg, like Rails' HashWithIndifferentAccess
2
+ class FancyHash < Hash
3
+ end
@@ -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(called_method)
13
- @called_methods = [called_method]
12
+ def initialize(called_methods)
13
+ @called_methods = called_methods
14
14
  end
15
15
 
16
16
  def method_missing(method, *args, &block)
17
- tap { called_methods << method }
17
+ self.class.new(called_methods + [method])
18
18
  end
19
-
20
- def is_a?(thing)
21
- thing == ::ActiveRecord::Relation ? true : super
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.0.0
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-28 00:00:00.000000000 Z
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