searchlight 1.1.0 → 1.2.0

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