searchlight 0.0.1 → 0.9.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
- OTA2YmIzYjk0NDhkNjdlNTg5MWU3MWQzOWExOTg0Mzg0Y2VmYjhkYw==
4
+ NGRhMjI1Mjg1ZDRmYTA5Yjc1YTk1ZWVkZWY5M2VhNjQ2YmQ0YzRhYQ==
5
5
  data.tar.gz: !binary |-
6
- N2I3MmFlOGEzMjFiODcyNjUxNzA1ZWUxZjg4YmY5OTk0OWZjZGNlOQ==
6
+ NTU2MjMxYTdmY2YwNTYxZDY4YzU0NTY2ZjczNGRjMjlmNTM3MWFiZg==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- OWUxMjY2OGYxMjNmZGJiOTgyYjY3OGVkYzgxOWI4NTQ2ZmI2YzA2N2MwYTA4
10
- NGQ3Y2FjYmM4MTRjZGU2OWY1YjQ3MDg3MmJjNDAxMzY1NWM2OTJjN2VlZDNh
11
- MzY5YjIzZjE2MGRkYzVmZTMyMTI3ZmU3MzVkMTU0MTliZjVmNzI=
9
+ OWE1NWE0ZDc3N2MyMGJiNzZmY2MwNGYzMzg4NWVmNjY4MWQyYzY5ZGEzZmQz
10
+ YjIwYmE4ZGJkZDhkNzhkNmM0MDNhODBiMzRhMTVjODY1MjM2NjJkMzhkNzZi
11
+ ODVmZDMyMGQ2N2Q1YWEwMDVlOTI0Zjg5YjBmYjYzZjZjOTU2M2U=
12
12
  data.tar.gz: !binary |-
13
- ODJjNGM2NmQzNWFjY2FlYjUwN2EzNGQwN2MzNjI2Njg4MjE2NWUxNzg3ZjJl
14
- ZDk4NzcxYzUzNzE4NWQ3ODg5NGZmZmUwOGM0ODljOTE5MDRiYWIzMmMzMDJl
15
- MTMzMWU2YmEwMWY3MjM2MzU1MjJlZTk0MDNlNjBmY2VjM2U5ZGQ=
13
+ YmNhN2M0M2VjMmM0MTZmMjBmOTI1ODJlMzE0NWI0YTVhZDA2ZDIzYWY0NTYx
14
+ YjFhM2QxMGZmZGY3MTFkNzg0NWY0YmMzN2MxMTViMjlmMWQyMTRiYjEzZTAy
15
+ MjMyZTY5YjRjYWM1NjI2ZTE0MjNmN2UyMWM2MDVmNzliMmMyMDU=
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - jruby-19mode # JRuby in 1.9 mode
5
+ - rbx-19mode # JRuby in 1.9 mode
6
+ - 2.0.0
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ Searchlight does its best to use [semantic versioning](http://semver.org).
4
+
5
+ ## v0.0.1
6
+
7
+ Experimental and unstable, Searchlight totters onto the scene and takes its first wide-eyed look at the world.
8
+
9
+ It is adorable.
data/README.md CHANGED
@@ -2,98 +2,253 @@
2
2
 
3
3
  Searchlight helps you build searches from options via Ruby methods that you write.
4
4
 
5
- Searchlight comes with ActiveRecord integration, but can call search methods on any ORM or object that allows chaining search methods.
5
+ Searchlight can work with any ORM or object that allows chaining search methods. It comes with modules for integrating with ActiveRecord and ActionView, but can easily be used in any Ruby program.
6
6
 
7
7
  [![Build Status](https://api.travis-ci.org/nathanl/searchlight.png?branch=master)](https://travis-ci.org/nathanl/searchlight)
8
8
  [![Code Climate](https://codeclimate.com/github/nathanl/searchlight.png)](https://codeclimate.com/github/nathanl/searchlight)
9
9
 
10
10
  ## Overview
11
11
 
12
- The basic idea of Searchlight is to build a search by chaining method calls that you define. It calls methods on the object you specify, based on the options you pass.
12
+ The basic idea of Searchlight is to build a search by chaining method calls that you define. It calls **public** methods on the object you specify, based on the options you pass.
13
13
 
14
- For example, if you have a Searchlight search class called `FooSearch`, and you instantiate it like this:
14
+ For example, if you have a Searchlight search class called `YetiSearch`, and you instantiate it like this:
15
15
 
16
16
  ```ruby
17
- foo_search = FooSearch(active: true, name: 'Jimmy', location_in: %w[NY LA]) # or params[:query]
17
+ yeti_search = YetiSearch(active: true, name: 'Jimmy', location_in: %w[NY LA]) # or params[:search]
18
18
  ```
19
19
 
20
- ... calling `results` will call the instance methods `search_active`, `search_name`, and `search_location_in`. (If you omit the `active` option, `search_active` won't be called.)
20
+ ... calling `results` on the instance will build a search by chaining calls to `search_active`, `search_name`, and `search_location`.
21
21
 
22
- The `results` method will then return the return value of the last search method. If you're using ActiveRecord, this would be an `ActiveRecord::Relation`. You can then call `each` to loop through the results, `to_sql` to get the generated query, etc.
22
+ The `results` method will then return the return value of the last search method. If you're using ActiveRecord, this would be an `ActiveRecord::Relation`, and you can then call `each` to loop through the results, `to_sql` to get the generated query, etc.
23
23
 
24
24
  ## Usage
25
25
 
26
26
  ### Search class
27
27
 
28
- Here's an example search class that uses ActiveRecord.
28
+ A search class has three main parts: a target, options, and methods. For example:
29
29
 
30
30
  ```ruby
31
- # app/searches/account_search.rb
32
- class AccountSearch < Searchlight::Search
31
+ class PersonSearch < Searchlight::Search
33
32
 
34
- # Defines the `search_target`
35
- search_on Account
33
+ # The search target; in this case, an ActiveRecord model.
34
+ search_on Person
36
35
 
37
- # The search options this class knows how to handle
38
- searches :contract_id, :invoicing_status, :active
36
+ # The options the search understands. Supply any combination of them to an instance.
37
+ searches :first_name, :last_name
39
38
 
40
- # If a `contract_id` option is given, this method will be called
41
- def search_contract_id
42
- search.where(contract_id: contract_id)
39
+ # A search method.
40
+ def search_first_name
41
+ # If this is the first search method called, `search` here will be
42
+ # the search target, namely, `Person`.
43
+ # `first_name` is an automatically-defined accessor for the option value.
44
+ search.where(first_name: first_name)
43
45
  end
44
46
 
45
- # If an `invoicing` option is given, this method will be called
46
- def search_invoicing
47
- case invoicing_status
48
- when 'partial'
49
- search.partially_invoiced
50
- when 'complete'
51
- search.completely_invoiced
52
- when 'never'
53
- search.uninvoiced
54
- else
55
- search
56
- end
47
+ # Another search method.
48
+ def search_last_name
49
+ # If this is the second search method called, `search` here will be
50
+ # whatever `search_first_name` returned.
51
+ search.where(last_name: last_name)
52
+ end
53
+ end
54
+ ```
55
+
56
+ Here's a fuller example search class.
57
+
58
+ ```ruby
59
+ # app/searches/city_search.rb
60
+ class CitySearch < Searchlight::Search
61
+
62
+ # `City` here is an ActiveRecord model (see notes below on the adapter)
63
+ search_on City.includes(:country)
64
+
65
+ searches :name, :continent, :country_name_like, :is_megacity
66
+
67
+ # Reach into other tables
68
+ def search_continent
69
+ search.where('`countries`.`continent` = ?', continent)
70
+ end
71
+
72
+ # Other kinds of queries
73
+ def search_country_name_like
74
+ search.where("`countries`.`name` LIKE ?", "%#{country_name_like}%")
57
75
  end
58
76
 
59
- # If an `active` option is given, this method will be called
60
- def search_active
61
- search.where(status: active? ? 'active' : 'inactive')
77
+ # For every option, we also add an accessor that coerces to a boolean,
78
+ # considering 'false', 0, and '0' to be false
79
+ def search_is_megacity
80
+ search.where("`cities`.`population` #{is_megacity? ? '>=' : '<'} ?", 10_000_000)
62
81
  end
63
82
 
64
83
  end
65
84
  ```
66
85
 
67
- ### Controller
86
+ Here are some example searches.
68
87
 
69
88
  ```ruby
70
- # app/controllers/accounts_controller.rb
71
- class AccountsController
89
+ CitySearch.new.results.to_sql
90
+ # => "SELECT `cities`.* FROM `cities` "
91
+ CitySearch.new(name: 'Nairobi').results.to_sql
92
+ # => "SELECT `cities`.* FROM `cities` WHERE `cities`.`name` = 'Nairobi'"
93
+
94
+ CitySearch.new(country_name_like: 'aust', continent: 'Europe').results.count # => 6
95
+
96
+ non_megas = CitySearch.new(is_megacity: 'false')
97
+ non_megas.results.to_sql
98
+ # => "SELECT `cities`.* FROM `cities` WHERE (`cities`.`population` < 100000"
99
+ non_megas.results.each do |city|
100
+ # ...
101
+ end
102
+ ```
103
+
104
+ ### Accessors
105
+
106
+ For each search option, Searchlight defines two accessors: one for a value, and one for a boolean.
107
+
108
+ For example, if your class `searches :awesomeness` and gets instantiated like:
109
+
110
+ ```ruby
111
+ search = MySearchClass(awesomeness: 'Xtreme')
112
+ ```
113
+
114
+ ... your search methods can use:
115
+
116
+ - `awesomeness` to retrive the given value, `'Xtreme'`
117
+ - `awesomeness?` to get a boolean version: `true`
118
+
119
+ The boolean conversion is form-friendly, so that `0`, `'0'`, and `'false'` are considered `false`.
120
+
121
+ All accessors are defined in modules, so you can override them and use `super` to call the original methods.
122
+
123
+ ```ruby
124
+ class PersonSearch < Searchlight::Search
125
+
126
+ searches :names, :awesomeness
127
+
128
+ def names
129
+ # Make sure this is an array and never search for Jimmy.
130
+ # Jimmy is a private man. An old-fashioned man. Leave him be.
131
+ Array(super).reject { |name| name == 'Jimmy' }
132
+ end
133
+
134
+ def searches_names
135
+ search.where("name IN (?)", names)
136
+ end
137
+
138
+ def awesomeness?
139
+ # Disagree about what is awesome
140
+ !super
141
+ end
72
142
 
73
- def search
74
- @search = AccountSearch.new(params[:search])
75
143
  end
76
- ...
77
144
  ```
78
145
 
79
- ### View
146
+ ### Defining Defaults
147
+
148
+ ```ruby
149
+
150
+ class CitySearch < Searchlight::Search
151
+
152
+ #...
153
+
154
+ def initialize(options = {})
155
+ super
156
+ self.continent ||= 'Asia'
157
+ end
158
+
159
+ #...
160
+ end
161
+
162
+ CitySearch.new.results.to_sql
163
+ => "SELECT `cities`.* FROM `cities` WHERE (`countries`.`continent` = 'Asia')"
164
+ CitySearch.new(continent: 'Europe').results.to_sql
165
+ => "SELECT `cities`.* FROM `cities` WHERE (`countries`.`continent` = 'Europe')"
166
+ ```
167
+
168
+ ### Subclassing
169
+
170
+ You can subclass an existing search class and support all the same options with a different search target. This may be useful for single table inheritance, for example.
171
+
80
172
  ```ruby
81
- # app/views/accounts/index.html.haml
173
+ class VillageSearch < CitySearch
174
+ search_on Village
175
+ end
176
+ ```
177
+
178
+ You can also use `search_target` to get the superclass's `search_on` value, so you can do this:
179
+
180
+ ```ruby
181
+ class SmallTownSearch < CitySearch
182
+ search_on search_target.where("`cities`.`population` < ?", 1_000)
183
+ end
184
+
185
+ SmallTownSearch.new(country_name_like: 'Norfolk').results.to_sql
186
+ => "SELECT `cities`.* FROM `cities` WHERE (`cities`.`population` < 1000) AND (`countries`.`name` LIKE '%Norfolk%')"
187
+ ```
188
+
189
+ ## Usage in Rails
190
+
191
+ Searchlight plays nicely with Rails views.
192
+
193
+ ```ruby
194
+ # app/views/cities/index.html.haml
82
195
  ...
83
- = form_for(search, url: '#') do |f|
196
+ = form_for(@search, url: search_cities_path) do |f|
197
+ %fieldset
198
+ = f.label :name, "Name"
199
+ = f.text_field :name
200
+
84
201
  %fieldset
85
- = f.label :contract_id, "Contract"
86
- = f.select :contract_id, available_contracts_collection
202
+ = f.label :country_name_like, "Country Name Like"
203
+ = f.text_field :country_name_like
87
204
 
88
205
  %fieldset
89
- = f.label :invoicing_status, "Invoicing Status"
90
- = f.select :invoicing_status, invoice_statuses_collection
206
+ = f.label :is_megacity, "Megacity?"
207
+ = f.select :is_megacity, [['Yes', true], ['No', false], ['Either', '']]
91
208
 
92
209
  %fieldset
93
- = f.label :active, "Active?"
94
- = f.select :active, [['Active', true], ['Inactive', false], ['Either', nil]]
210
+ = f.label :continent, "Continent"
211
+ = f.select :continent, ['Africa', 'Asia', 'Europe'], include_blank: true
212
+
213
+ = f.submit "Search"
95
214
  ```
96
215
 
216
+ As long as your form submits options your search understands, you can easily hook it up in your controller:
217
+
218
+ ```ruby
219
+ # app/controllers/cities_controller.rb
220
+ class CitiesController < ApplicationController
221
+ def search
222
+ @search = CitySearch.new(params[:search])
223
+ end
224
+ end
225
+ ```
226
+ ## Adapters
227
+
228
+ Currently, Searchlight has adapters for ActiveRecord and ActionView. We'd love to get pull requests for others. :)
229
+
230
+ ### ActiveRecord
231
+
232
+ When you call `search_on` in your Searchlight class, Searchlight checks whether the search target comes from ActiveRecord, and, if so, mixes a module into your class.
233
+
234
+ For each of your search options, the module will have the simplest possible search method defined. For example, if your class `searches :name`, the module will have this method:
235
+
236
+ ```ruby
237
+ def search_name
238
+ search.where(name: name)
239
+ end
240
+ ```
241
+
242
+ Since that method is in a parent module, you can easily override it by defining your own method. You can also call `super` in the method you define.
243
+
244
+ ### ActionView
245
+
246
+ Similarly, Searchlight adds ActionView-friendly methods to your classes if it sees that `ActionView` is a defined constant. See the code for details, but the upshot is that you can use a search with `form_for`.
247
+
248
+ ## Compatibility
249
+
250
+ For any given version, check `.travis.yml` to see what Ruby versions we're testing for compatibility.
251
+
97
252
  ## Installation
98
253
 
99
254
  Add this line to your application's Gemfile:
@@ -115,3 +270,8 @@ Or install it yourself as:
115
270
  3. Commit your changes (`git commit -am 'Add some feature'`)
116
271
  4. Push to the branch (`git push origin my-new-feature`)
117
272
  5. Create new Pull Request
273
+
274
+ ## Shout Outs
275
+
276
+ - The excellent [Mr. Adam Hunter](https://github.com/adamhunter), co-creator of Searchlight.
277
+ - [TMA](http://tma1.com) for supporting the development of Searchlight.
data/TODO.md ADDED
@@ -0,0 +1,5 @@
1
+ # TODO
2
+
3
+ - Run rcov and mutant
4
+ - Make nice Github page
5
+ - Test with ActiveRecord 4
@@ -5,7 +5,7 @@ module Searchlight
5
5
  module ClassMethods
6
6
 
7
7
  def model_name
8
- ActiveModel::Name.new(self, nil, 'query')
8
+ ActiveModel::Name.new(self, nil, 'search')
9
9
  end
10
10
 
11
11
  end
@@ -16,6 +16,10 @@ module Searchlight
16
16
  []
17
17
  end
18
18
 
19
+ def persisted?
20
+ false
21
+ end
22
+
19
23
  end
20
24
 
21
25
  end
@@ -4,22 +4,28 @@ module Searchlight
4
4
 
5
5
  def search_on(target)
6
6
  super
7
- extend Search if target.is_a?(::ActiveRecord::Base)
7
+ extend Search if target.is_a?(::ActiveRecord::Base) || target.is_a?(::ActiveRecord::Relation)
8
8
  end
9
9
 
10
10
  module Search
11
11
  def searches(*attribute_names)
12
12
  super
13
13
 
14
- include_new_module "SearchlightActiveRecordSearches" do
15
- attribute_names.each do |attribute_name|
16
- define_method("search_#{attribute_name}") do
17
- search.where(attribute_name => public_send(attribute_name))
18
- end
19
- end
14
+ # Ensure this class only adds one search module to the ancestors chain
15
+ if @ar_searches_module.nil?
16
+ @ar_searches_module = Named::Module.new("SearchlightActiveRecordSearches(#{self})")
17
+ include @ar_searches_module
20
18
  end
21
19
 
22
- attribute_names.each { |attribute_name| method_added("search_#{attribute_name}") }
20
+ eval_string = attribute_names.map { |attribute_name|
21
+ <<-UNICORN_BILE
22
+ def search_#{attribute_name}
23
+ search.where('#{attribute_name}' => public_send("#{attribute_name}"))
24
+ end
25
+ UNICORN_BILE
26
+ }.join
27
+
28
+ @ar_searches_module.module_eval(eval_string, __FILE__, __LINE__)
23
29
  end
24
30
  end
25
31
 
@@ -6,25 +6,28 @@ module Searchlight
6
6
  end
7
7
 
8
8
  def searches(*attribute_names)
9
- include_new_module "SearchlightAccessors" do
10
- attr_accessor *attribute_names
11
9
 
12
- # define boolean accessors
13
- attribute_names.each do |attribute_name|
14
- define_method("#{attribute_name}?") do
15
- # Treat 0 (eg, from checkboxes) as false
16
- !['0', 'false', ''].include?(public_send(attribute_name).to_s.strip)
10
+ # Ensure this class only adds one accessors module to the ancestors chain
11
+ if @accessors_module.nil?
12
+ @accessors_module = Named::Module.new("SearchlightAccessors(#{self})") do
13
+ private
14
+ # Treat 0 (eg, from checkboxes) as false
15
+ def truthy?(value)
16
+ !(['0', 'false', ''].include?(value.to_s.strip))
17
17
  end
18
18
  end
19
+ include @accessors_module
19
20
  end
20
- end
21
-
22
- private
23
21
 
24
- # So that we can allow calling `super` in submodules and the base class.
25
- def include_new_module(module_name, &content)
26
- include Named::Module.new(module_name, &content)
22
+ eval_string = "attr_accessor *#{attribute_names}\n"
23
+ eval_string << attribute_names.map { |attribute_name|
24
+ <<-LEPRECHAUN_JUICE
25
+ def #{attribute_name}?
26
+ truthy?(public_send("#{attribute_name}"))
27
+ end
28
+ LEPRECHAUN_JUICE
29
+ }.join
30
+ @accessors_module.module_eval(eval_string, __FILE__, __LINE__)
27
31
  end
28
-
29
32
  end
30
33
  end
@@ -1,5 +1,3 @@
1
- require 'set'
2
-
3
1
  module Searchlight
4
2
  class Search
5
3
  extend DSL
@@ -8,17 +6,10 @@ module Searchlight
8
6
  defined?(@search_target) ? @search_target : superclass.search_target
9
7
  end
10
8
 
11
- def self.search_methods
12
- defined?(@search_methods) ? @search_methods : superclass.search_methods
13
- end
14
-
15
- def self.method_added(name)
16
- @search_methods ||= Set.new
17
- search_methods << name.to_s if name.to_s.start_with?('search_')
18
- end
19
-
20
9
  def initialize(options = {})
21
- options.each { |key, value| public_send("#{key}=", value) }
10
+ options.each { |key, value| public_send("#{key}=", value) } if options && options.any?
11
+ rescue NoMethodError => e
12
+ raise UndefinedOption.new(e.name, self.class.name)
22
13
  end
23
14
 
24
15
  def search
@@ -35,15 +26,44 @@ module Searchlight
35
26
 
36
27
  private
37
28
 
29
+ def search_methods
30
+ public_methods.map(&:to_s).select { |m| m.start_with?('search_') }
31
+ end
32
+
38
33
  def run
39
- self.class.search_methods.each do |method|
40
- option_value = public_send(method.sub(/\Asearch_/, ''))
41
- unless option_value.nil? || option_value.to_s.strip == ''
42
- self.search = public_send(method)
43
- end
34
+ search_methods.each do |method|
35
+ new_search = run_search_method(method)
36
+ self.search = new_search unless new_search.nil?
44
37
  end
45
38
  search
46
39
  end
47
40
 
41
+ def run_search_method(method_name)
42
+ option_value = instance_variable_get("@#{method_name.sub(/\Asearch_/, '')}")
43
+ option_value = option_value.reject { |item| blank_value?(item) } if option_value.respond_to?(:reject)
44
+ public_send(method_name) unless blank_value?(option_value)
45
+ end
46
+
47
+ # Note that false is not blank
48
+ def blank_value?(value)
49
+ (value.respond_to?(:empty?) && value.empty?) || value.nil? || value.to_s.strip == ''
50
+ end
51
+
52
+ class UndefinedOption < StandardError
53
+ attr_accessor :message
54
+ def initialize(option_name, search_class)
55
+ option_name = option_name.to_s.sub(/=\Z/, '')
56
+ self.message = "#{search_class} doesn't search '#{option_name}'."
57
+ if option_name.start_with?('search_')
58
+ # Gee golly, I'm so helpful!
59
+ self.message << " Did you just mean '#{option_name.sub(/\Asearch_/, '')}'?"
60
+ end
61
+ end
62
+
63
+ def to_s
64
+ message
65
+ end
66
+ end
67
+
48
68
  end
49
69
  end
@@ -1,3 +1,3 @@
1
1
  module Searchlight
2
- VERSION = "0.0.1"
2
+ VERSION = "0.9.0"
3
3
  end
data/lib/searchlight.rb CHANGED
@@ -2,6 +2,7 @@ require 'named'
2
2
  require 'searchlight/version'
3
3
 
4
4
  module Searchlight
5
+ Error = Class.new(StandardError)
5
6
  end
6
7
 
7
8
  require 'searchlight/dsl'
data/searchlight.gemspec CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
8
8
  spec.version = Searchlight::VERSION
9
9
  spec.authors = ["Nathan Long", "Adam Hunter"]
10
10
  spec.email = ["nathanmlong@gmail.com", "adamhunter@me.com"]
11
- spec.description = %q{Searchlight helps you build searches from options via Ruby methods that you write.}
11
+ spec.description = %q{Searchlight helps you build searches from options via Ruby methods that you write. Searchlight can work with any ORM or object that allows chaining search methods. It comes with modules for integrating with ActiveRecord and ActionView, but can easily be used in any Ruby program.}
12
12
  spec.summary = %q{Searchlight helps you build searches from options via Ruby methods that you write.}
13
13
  spec.homepage = "https://github.com/nathanl/searchlight"
14
14
  spec.license = "MIT"
@@ -25,4 +25,5 @@ Gem::Specification.new do |spec|
25
25
  spec.add_development_dependency "rake"
26
26
  spec.add_development_dependency "rails", ">= 3"
27
27
  spec.add_development_dependency "capybara", "~> 2.0"
28
+ spec.add_development_dependency "simplecov", "~> 0.7"
28
29
  end
@@ -19,7 +19,11 @@ describe 'Searchlight::Adapters::ActionView', type: :feature, adapter: true do
19
19
  f.text_field(:paid_amount)
20
20
  end
21
21
 
22
- expect(form).to have_selector("form input[name='query[paid_amount]'][value='15']")
22
+ expect(form).to have_selector("form input[name='search[paid_amount]'][value='15']")
23
+ end
24
+
25
+ it "tells the form that it is not persisted" do
26
+ expect(search).not_to be_persisted
23
27
  end
24
28
 
25
29
  end
@@ -19,7 +19,7 @@ describe 'Searchlight::Adapters::ActiveRecord', adapter: true do
19
19
  end
20
20
 
21
21
  it "adds search_elephants to the search_methods array" do
22
- expect(search_class.search_methods).to include('search_elephants')
22
+ expect(search_instance.send(:search_methods)).to include('search_elephants')
23
23
  end
24
24
 
25
25
  it "defines search methods that call where on the search target" do
@@ -27,4 +27,9 @@ describe 'Searchlight::Adapters::ActiveRecord', adapter: true do
27
27
  expect(search_instance.search.called_methods).to eq([:where])
28
28
  end
29
29
 
30
+ it "sets arguments properly in the defined method" do
31
+ search_instance.search.should_receive(:where).with('elephants' => 'yes, please')
32
+ search_instance.search_elephants
33
+ end
34
+
30
35
  end
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe Searchlight::Search do
4
4
 
5
- let(:search_class) { Named::Class.new('SearchClass', described_class) }
5
+ let(:search_class) { Named::Class.new('ExampleSearch', described_class) }
6
6
  let(:options) { Hash.new }
7
7
  let(:search) { search_class.new(options) }
8
8
 
@@ -15,6 +15,16 @@ describe Searchlight::Search do
15
15
  expect(search.beak_color).to eq('mauve')
16
16
  end
17
17
 
18
+ describe "handling invalid options" do
19
+
20
+ let(:options) { {genus: 'Mellivora'} }
21
+
22
+ it "raises an error explaining that this search class doesn't search the given property" do
23
+ expect { search }.to raise_error( Searchlight::Search::UndefinedOption, /ExampleSearch.*genus/)
24
+ end
25
+
26
+ end
27
+
18
28
  end
19
29
 
20
30
  describe "search_on" do
@@ -44,7 +54,7 @@ describe Searchlight::Search do
44
54
  describe "search_methods" do
45
55
 
46
56
  let(:search_class) {
47
- Named::Class.new('SearchClass', described_class) do
57
+ Named::Class.new('ExampleSearch', described_class) do
48
58
  def search_bees
49
59
  end
50
60
 
@@ -57,7 +67,7 @@ describe Searchlight::Search do
57
67
  }
58
68
 
59
69
  it "keeps a unique list of the search methods" do
60
- expect(search_class.search_methods).to eq(Set.new(['search_bees', 'search_bats']))
70
+ expect(search.send(:search_methods).map(&:to_s).sort).to eq(['search_bats', 'search_bees'])
61
71
  end
62
72
 
63
73
  end
@@ -68,11 +78,14 @@ describe Searchlight::Search do
68
78
 
69
79
  before :each do
70
80
  search_class.searches :foo
81
+ search_class.searches :bar
82
+ search_class.searches :stuff
71
83
  end
72
84
 
73
- it "includes a SearchlightAccessors module" do
74
- accessors_module = search_class.ancestors.detect {|a| a.name == 'SearchlightAccessors' }
75
- expect(accessors_module).to be_a(Named::Module)
85
+ it "includes exactly one SearchlightAccessors module for this class" do
86
+ accessors_modules = search_class.ancestors.select {|a| a.name =~ /\ASearchlightAccessors/ }
87
+ expect(accessors_modules.length).to eq(1)
88
+ expect(accessors_modules.first).to be_a(Named::Module)
76
89
  end
77
90
 
78
91
  it "adds a getter" do
data/spec/spec_helper.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  require 'capybara/rspec'
2
+ require 'simplecov'
3
+ SimpleCov.start
2
4
  require 'searchlight'
3
5
  $LOAD_PATH << '.'
4
6
  require 'support/mock_model'
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: 0.0.1
4
+ version: 0.9.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-04-08 00:00:00.000000000 Z
12
+ date: 2013-04-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: named
@@ -95,8 +95,24 @@ dependencies:
95
95
  - - ~>
96
96
  - !ruby/object:Gem::Version
97
97
  version: '2.0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: simplecov
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ~>
103
+ - !ruby/object:Gem::Version
104
+ version: '0.7'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ~>
110
+ - !ruby/object:Gem::Version
111
+ version: '0.7'
98
112
  description: Searchlight helps you build searches from options via Ruby methods that
99
- you write.
113
+ you write. Searchlight can work with any ORM or object that allows chaining search
114
+ methods. It comes with modules for integrating with ActiveRecord and ActionView,
115
+ but can easily be used in any Ruby program.
100
116
  email:
101
117
  - nathanmlong@gmail.com
102
118
  - adamhunter@me.com
@@ -106,10 +122,13 @@ extra_rdoc_files: []
106
122
  files:
107
123
  - .gitignore
108
124
  - .rspec
125
+ - .travis.yml
126
+ - CHANGELOG.md
109
127
  - Gemfile
110
128
  - LICENSE.txt
111
129
  - README.md
112
130
  - Rakefile
131
+ - TODO.md
113
132
  - lib/searchlight.rb
114
133
  - lib/searchlight/adapters/action_view.rb
115
134
  - lib/searchlight/adapters/active_record.rb