searchlight 1.1.0 → 1.2.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,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- MjNmMzE5OWE2YzM4N2ZjMDU4YWNmNGZmZTk5NzM2NGQxNWRjYzNjYw==
4
+ MmFhNGNkYmJmZDdmMjc5NjJjODI3NzZmN2Q0ODY5NjkyNzYwZjhiZQ==
5
5
  data.tar.gz: !binary |-
6
- Y2IzNWVmNzUyOTU4NjYxYjU2YmRiNWIzZDBjZmE0YTgyMzZlMTgyNw==
6
+ YzRlZTAyZTZiN2UzOTM3NWU1YTAyZGRkNDM1YjUwMTA0NTAxMGU4ZQ==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- MjE2ZTMxMDM4OTQ0NDlmNjk5MWY4ODBlMmJkNDAwZDMzZjg4Njc1NDJmZWIz
10
- Y2UzNTIxMzg2M2E1Mzk4OGM1MzdhNmYwOGFlOGM1MDg1ZmU0MWJjNDRmYWY5
11
- Y2IxOTM2YzU1OTRjZWU4MjhkM2FlZjQxZGU3YTU1YTcwMjc0MjI=
9
+ ZTY3MjgyOGI3Y2FjYTFhMzlhZGVhMGQyYmNiYTRmZDI1Nzc3MzY2YjU1Mzcw
10
+ M2Y4YjFmNDg3YmEzZmUyNWM0ZGYwOTNkZTYyODkyZGNmZTU4NTUyMWQwNmQ3
11
+ M2U3NDBkOTM4OTRhZGFiNTNhMDQxOTdlYmFiZTEwMTQ1MzlmYjI=
12
12
  data.tar.gz: !binary |-
13
- MGQ0ZDg1YmMyZmJjYjcxNmY0ODU3MGNkNDY5YmY4NzkyOGJjMDExZThjNzk0
14
- YjRlZDc0ODRiNTI4ZDgyNjFjNzEzOWU4ZTNjOGVlZjY5M2Y3YmI1NGYzM2My
15
- ZGIwNTBlNzg4ZDM3ZWY4NTZmNDU0NjJiNWQ2ZTY3ODNiN2RkNmU=
13
+ MDRkNDY3N2Y4Mzc4Mjc2NGZjOGYxYWZlMjc3MjZmYzM4ZmFlYTE0NzMyYWIy
14
+ YTQwZjhiNjQwM2I3OTI3Zjk3NTA4MzQ1ZTM1NTE3Mzk4NTliMDc1NWVkODRm
15
+ YzZmZTg0YmUzZDgzMDI3YjYzN2RmZjFiYTBiM2EwMmIyNTUyNzc=
data/.gitignore CHANGED
@@ -3,6 +3,8 @@
3
3
  .bundle
4
4
  .config
5
5
  .rvmrc
6
+ .ruby-version
7
+ .ruby-gemset
6
8
  .yardoc
7
9
  Gemfile.lock
8
10
  InstalledFiles
data/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  Searchlight does its best to use [semantic versioning](http://semver.org).
4
4
 
5
+ ## v1.2.0
6
+
7
+ - Provide `options` accessor that returns all options considered non-blank
8
+ - Slightly nicer errors when passing invalid options
9
+
5
10
  ## v1.1.0
6
11
 
7
12
  ActiveRecord adapter ensures that searches return a relation, even if no options are given
data/Gemfile CHANGED
@@ -2,3 +2,4 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in searchlight.gemspec
4
4
  gemspec
5
+ gem 'coveralls', require: false
data/README.md CHANGED
@@ -4,8 +4,11 @@ Searchlight helps you build searches from options via Ruby methods that you writ
4
4
 
5
5
  Searchlight can work with any ORM or object that can build a query using chained methods (eg, ActiveRecord's `.where(...).where(...).limit(...)`). It comes with modules for integrating with ActiveRecord and ActionView, but can easily be used in any Ruby program.
6
6
 
7
- [![Build Status](https://api.travis-ci.org/nathanl/searchlight.png?branch=master)](https://travis-ci.org/nathanl/searchlight)
7
+ [![Gem Version](https://badge.fury.io/rb/searchlight.png)](https://rubygems.org/gems/searchlight)
8
8
  [![Code Climate](https://codeclimate.com/github/nathanl/searchlight.png)](https://codeclimate.com/github/nathanl/searchlight)
9
+ [![Build Status](https://api.travis-ci.org/nathanl/searchlight.png?branch=master)](https://travis-ci.org/nathanl/searchlight)
10
+ [![Coverage Status](https://coveralls.io/repos/nathanl/searchlight/badge.png?branch=master)](https://coveralls.io/r/nathanl/searchlight?branch=master)
11
+ [![Dependency Status](https://gemnasium.com/nathanl/searchlight.png)](https://gemnasium.com/nathanl/searchlight)
9
12
 
10
13
  ## Overview
11
14
 
@@ -103,7 +106,7 @@ end
103
106
 
104
107
  ### Accessors
105
108
 
106
- For each search option, Searchlight defines two accessors: one for a value, and one for a boolean.
109
+ For each search option you allow, Searchlight defines two accessors: one for a value, and one for a boolean.
107
110
 
108
111
  For example, if your class `searches :awesomeness` and gets instantiated like:
109
112
 
@@ -143,11 +146,12 @@ class PersonSearch < Searchlight::Search
143
146
  end
144
147
  ```
145
148
 
149
+ 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.
150
+
146
151
  ### Defining Defaults
147
152
 
148
153
  Set defaults using plain Ruby. These can be used to prefill a form or to assume what the user didn't specify.
149
154
 
150
-
151
155
  ```ruby
152
156
 
153
157
  class CitySearch < Searchlight::Search
@@ -252,6 +256,9 @@ Searchlight plays nicely with Rails forms. All search options and any `attr_acce
252
256
  = f.select :continent, ['Africa', 'Asia', 'Europe'], include_blank: true
253
257
 
254
258
  = f.submit "Search"
259
+
260
+ - @results.each do |city|
261
+ = render 'city'
255
262
  ```
256
263
 
257
264
  ### Controllers
@@ -263,7 +270,8 @@ As long as your form submits options your search understands, you can easily hoo
263
270
  class OrdersController < ApplicationController
264
271
 
265
272
  def index
266
- @orders = OrderSearch.new(search_params)
273
+ @search = OrderSearch.new(search_params) # For use in a form
274
+ @results = @search.results # For display along with form
267
275
  end
268
276
 
269
277
  protected
@@ -2,6 +2,8 @@ module Searchlight
2
2
  class Search
3
3
  extend DSL
4
4
 
5
+ attr_accessor :options
6
+
5
7
  def self.search_target
6
8
  return @search_target if defined?(@search_target)
7
9
  return superclass.search_target if superclass.respond_to?(:search_target) && superclass != Searchlight::Search
@@ -9,9 +11,7 @@ module Searchlight
9
11
  end
10
12
 
11
13
  def initialize(options = {})
12
- options.each { |key, value| public_send("#{key}=", value) } if options && options.any?
13
- rescue NoMethodError => e
14
- raise UndefinedOption.new(e.name, self.class.name)
14
+ filter_and_mass_assign(options)
15
15
  end
16
16
 
17
17
  def search
@@ -45,26 +45,25 @@ module Searchlight
45
45
  @search_target = value
46
46
  end
47
47
 
48
- def search_methods
49
- public_methods.map(&:to_s).select { |m| m.start_with?('search_') }
48
+ def filter_and_mass_assign(provided_options)
49
+ self.options = provided_options.reject { |key, value| is_blank?(value) }
50
+ begin
51
+ options.each { |key, value| public_send("#{key}=", value) } if options && options.any?
52
+ rescue NoMethodError => e
53
+ raise UndefinedOption.new(e.name, self)
54
+ end
50
55
  end
51
56
 
52
57
  def run
53
- search_methods.each do |method|
54
- new_search = run_search_method(method)
58
+ options.each do |option_name, value|
59
+ new_search = public_send("search_#{option_name}") if respond_to?("search_#{option_name}")
55
60
  self.search = new_search unless new_search.nil?
56
61
  end
57
62
  search
58
63
  end
59
64
 
60
- def run_search_method(method_name)
61
- option_value = instance_variable_get("@#{method_name.sub(/\Asearch_/, '')}")
62
- option_value = option_value.reject { |item| blank_value?(item) } if option_value.respond_to?(:reject)
63
- public_send(method_name) unless blank_value?(option_value)
64
- end
65
-
66
65
  # Note that false is not blank
67
- def blank_value?(value)
66
+ def is_blank?(value)
68
67
  (value.respond_to?(:empty?) && value.empty?) || value.nil? || value.to_s.strip == ''
69
68
  end
70
69
 
@@ -74,12 +73,13 @@ module Searchlight
74
73
 
75
74
  attr_accessor :message
76
75
 
77
- def initialize(option_name, search_class)
76
+ def initialize(option_name, search)
78
77
  option_name = option_name.to_s.sub(/=\Z/, '')
79
- self.message = "#{search_class} doesn't search '#{option_name}'."
78
+ self.message = "#{search.class.name} doesn't search '#{option_name}' or have an accessor for that property."
80
79
  if option_name.start_with?('search_')
80
+ method_maybe_intended = option_name.sub(/\Asearch_/, '')
81
81
  # Gee golly, I'm so helpful!
82
- self.message << " Did you just mean '#{option_name.sub(/\Asearch_/, '')}'?"
82
+ self.message << " Did you just mean '#{method_maybe_intended}'?" if search.respond_to?("#{method_maybe_intended}=")
83
83
  end
84
84
  end
85
85
 
@@ -1,3 +1,3 @@
1
1
  module Searchlight
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0"
3
3
  end
@@ -26,10 +26,6 @@ describe 'Searchlight::Adapters::ActiveRecord', adapter: true do
26
26
  expect(search_class.new).to respond_to(:search_elephants)
27
27
  end
28
28
 
29
- it "adds search_elephants to the search_methods array" do
30
- expect(search_instance.send(:search_methods)).to include('search_elephants')
31
- end
32
-
33
29
  it "defines search methods that call where on the search target" do
34
30
  search_instance.results
35
31
  expect(search_instance.search.called_methods).to include(:where)
@@ -2,33 +2,86 @@ require 'spec_helper'
2
2
 
3
3
  describe Searchlight::Search do
4
4
 
5
- let(:search_class) { Named::Class.new('ExampleSearch', described_class) }
6
- let(:options) { Hash.new }
7
- let(:search) { search_class.new(options) }
5
+ let(:search_class) { Named::Class.new('ExampleSearch', described_class).tap { |klass| klass.searches *allowed_options } }
6
+ let(:allowed_options) { Hash.new }
7
+ let(:provided_options) { Hash.new }
8
+ let(:search) { search_class.new(provided_options) }
8
9
 
9
10
  describe "initializing" do
10
11
 
11
- let(:options) { {beak_color: 'mauve'} }
12
+ describe "mass-assigning provided options" do
13
+
14
+ let(:allowed_options) { [:beak_color] }
15
+ let(:provided_options) { {beak_color: 'mauve'} }
16
+
17
+ it "mass-assigns provided options" do
18
+ search_class.searches :beak_color
19
+ expect(search.beak_color).to eq('mauve')
20
+ end
21
+
22
+ end
23
+
24
+ describe "screening options" do
25
+
26
+ let(:allowed_options) { [:name, :description, :categories, :nicknames] }
27
+
28
+ context "when non-empty options are provided" do
29
+
30
+ let(:provided_options) { {name: 'Roy', description: 'Ornry', categories: %w[mammal moonshiner], nicknames: %w[Slim Bubba]} }
31
+
32
+ it "adds them to the options accessor" do
33
+ expect(search.options).to eq(provided_options)
34
+ end
35
+
36
+ end
37
+
38
+ context "when some provided options are empty" do
39
+
40
+ let(:provided_options) { {name: 'Roy', description: '', categories: %w[mammal moonshiner], nicknames: []} }
41
+
42
+ it "does not add them to the options accessor" do
43
+ expect(search.options).to eq(name: 'Roy', categories: %w[mammal moonshiner])
44
+
45
+ end
46
+
47
+ end
12
48
 
13
- it "mass-assigns provided options" do
14
- search_class.searches :beak_color
15
- expect(search.beak_color).to eq('mauve')
16
49
  end
17
50
 
18
51
  describe "handling invalid options" do
19
52
 
20
- let(:options) { {genus: 'Mellivora'} }
53
+ let(:provided_options) { {genus: 'Mellivora'} }
21
54
 
22
55
  it "raises an error explaining that this search class doesn't search the given property" do
23
56
  expect { search }.to raise_error( Searchlight::Search::UndefinedOption, /ExampleSearch.*genus/)
24
57
  end
25
58
 
26
- context "if the option starts with 'search_'" do
59
+ context "if the provided option starts with 'search_'" do
60
+
61
+ let(:allowed_options) { [:genus] }
62
+
63
+ context "and it looks like a valid search option" do
27
64
 
28
- let(:options) { {search_genus: 'Mellivora'} }
65
+ let(:provided_options) { {search_genus: 'Mellivora'} }
66
+
67
+ it "suggests the option name the user may have meant to provide" do
68
+ expect { search }.to raise_error( Searchlight::Search::UndefinedOption, /ExampleSearch.*genus.*Did you just mean/)
69
+ end
70
+
71
+ end
72
+
73
+ context "but doesn't look like a valid search option" do
74
+
75
+ let(:provided_options) { {search_girth: 'Wee'} }
76
+
77
+ it "doesn't suggest an option name" do
78
+ begin
79
+ search
80
+ rescue Searchlight::Search::UndefinedOption => exception
81
+ expect(exception.message.match(/Did you just mean/)).to be_nil
82
+ end
83
+ end
29
84
 
30
- it "suggests the option name the user may have meant to provide" do
31
- expect { search }.to raise_error( Searchlight::Search::UndefinedOption, /ExampleSearch.*genus.*Did you just mean/)
32
85
  end
33
86
 
34
87
  end
@@ -101,27 +154,6 @@ describe Searchlight::Search do
101
154
 
102
155
  end
103
156
 
104
- describe "search_methods" do
105
-
106
- let(:search_class) {
107
- Named::Class.new('ExampleSearch', described_class) do
108
- def search_bees
109
- end
110
-
111
- def search_bats
112
- end
113
-
114
- def search_bees
115
- end
116
- end
117
- }
118
-
119
- it "keeps a unique list of the search methods" do
120
- expect(search.send(:search_methods).map(&:to_s).sort).to eq(['search_bats', 'search_bees'])
121
- end
122
-
123
- end
124
-
125
157
  describe "search options" do
126
158
 
127
159
  describe "accessors" do
@@ -154,7 +186,7 @@ describe Searchlight::Search do
154
186
 
155
187
  describe "accessing search options as booleans" do
156
188
 
157
- let(:options) { {fishies: fishies} }
189
+ let(:provided_options) { {fishies: fishies} }
158
190
 
159
191
  before :each do
160
192
  search_class.searches :fishies
@@ -202,9 +234,12 @@ describe Searchlight::Search do
202
234
 
203
235
  describe "results" do
204
236
 
205
- let(:search) { AccountSearch.new(paid_amount: 50, business_name: "Rod's Meat Shack") }
237
+ let(:search) { AccountSearch.new(paid_amount: 50, business_name: "Rod's Meat Shack", other_attribute: 'whatevs') }
206
238
 
207
- it "builds a search by calling all of the methods that had values to search" do
239
+ it "builds a search by calling each search method that corresponds to a provided option" do
240
+ search.should_receive(:search_paid_amount).and_call_original
241
+ search.should_receive(:search_business_name).and_call_original
242
+ # Can't do `.should_not_receive(:search_other_attribute)` because the expectation defines a method which would get called.
208
243
  search.results
209
244
  expect(search.search.called_methods).to eq(2.times.map { :where })
210
245
  end
data/spec/spec_helper.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require 'capybara/rspec'
2
2
  require 'simplecov'
3
3
  SimpleCov.start
4
+ require 'coveralls'
5
+ Coveralls.wear!
4
6
  require 'searchlight'
5
7
  $LOAD_PATH << '.'
6
8
  require 'support/mock_model'
@@ -3,6 +3,7 @@ class AccountSearch < Searchlight::Search
3
3
  search_on MockModel
4
4
 
5
5
  searches :paid_amount, :business_name, :balance, :active
6
+ attr_accessor :other_attribute
6
7
 
7
8
  def search_paid_amount
8
9
  search.where('amount > ?', paid_amount)
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: 1.1.0
4
+ version: 1.2.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: 2013-05-20 00:00:00.000000000 Z
12
+ date: 2013-06-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: named