search_object 1.0 → 1.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: 4102172d9cbe97e6fe2e066614a3136ac194a481
4
- data.tar.gz: a01b6a7ebfa07a71a310ff32711147f378aaa337
3
+ metadata.gz: 24b14c3e7ec2492dc899f0a81a022089073be982
4
+ data.tar.gz: 8bee82cac11549acebcc9d35cc9cbb2aac25f456
5
5
  SHA512:
6
- metadata.gz: 28e608b9331e641f3d421293b7161697da27567d8f852453b629ddd476c6a1bb2da382677c46e579beaea92d9aca96b1ee3802cad1b81231b54ae041f4481aca
7
- data.tar.gz: 13d7e9ac89f8d9085cadf97a60ed102425a9ed877f5ecf2819c324b4003634d45584d068e52e161fafe61b5d48454452fc3fcc7926fd1a1d9ddd4a18a2c1a27b
6
+ metadata.gz: ba172f5b05a65e43bbbb3c3ab28e4b62c397757a6e9e1cb2bbe9e1b78a1fd94bba980d7d86ed1c286471adefe2bc99bacbb82c37f085f541ce2f407ecb2aa410
7
+ data.tar.gz: e5dc94ed98f6f947dc7a2f44b854ebc1a4bfe98000d00d6451200855860ce0ccc51a4b844e6d09638137038f3f3786c12d6a46a0d8d29bc80ef01558ed2ebb1d
data/.rubocop.yml ADDED
@@ -0,0 +1,24 @@
1
+ require: rubocop-rspec
2
+
3
+ AllCops:
4
+ RunRailsCops: true
5
+ Exclude:
6
+ - example/db/**/*
7
+ - example/config/**/*
8
+ - search_object.gemspec
9
+
10
+ # Disables "Line is too long"
11
+ LineLength:
12
+ Enabled: false
13
+
14
+ # Disables "Missing top-level class documentation comment"
15
+ Documentation:
16
+ Enabled: false
17
+
18
+ # Disables "Use each_with_object instead of inject"
19
+ Style/EachWithObject:
20
+ Enabled: false
21
+
22
+ # Disables "Prefer reduce over inject."
23
+ Style/CollectionMethods:
24
+ Enabled: false
data/.travis.yml CHANGED
@@ -1,8 +1,10 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.9.3
4
3
  - 2.0
5
4
  - 2.1
6
- script: bundle exec rspec spec
5
+ - 2.2
6
+ script:
7
+ - bundle exec rubocop
8
+ - bundle exec rspec spec
7
9
  notifications:
8
10
  email: false
data/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # Changelog
2
2
 
3
+ ## Version 1.1
4
+
5
+ * Search objects now can be inherited
6
+
7
+ ```ruby
8
+ class BaseSearch
9
+ include SearchObject.module
10
+
11
+ # ... options and configuration
12
+ end
13
+
14
+ class ProductSearch < BaseSearch
15
+ scope { Product }
16
+ end
17
+ ```
18
+
19
+ * Using instance method for straight dispatch
20
+
21
+ ```ruby
22
+ class ProductSearch
23
+ include SearchObject.module
24
+
25
+ scope { Product.all }
26
+
27
+ option :name
28
+ option :category_name
29
+
30
+ attr_reader :page
31
+
32
+ def initialize(filters = {}, page = 0)
33
+ super filters
34
+ @page = page.to_i.abc
35
+ end
36
+
37
+ def fetch_results
38
+ super.paginate page: @page
39
+ end
40
+ end
41
+ ```
42
+
3
43
  ## Version 1.0
4
44
 
5
45
  * Added min_per_page and max_per_page to paging plugin
data/README.md CHANGED
@@ -232,6 +232,24 @@ class ProductSearch
232
232
  end
233
233
  ```
234
234
 
235
+ ### Using instance method for straight dispatch
236
+
237
+ ```ruby
238
+ class ProductSearch
239
+ include SearchObject.module
240
+
241
+ scope { Product.all }
242
+
243
+ option :date, with: :parse_dates
244
+
245
+ private
246
+
247
+ def parse_dates(scope, value)
248
+ # some "magic" method to parse dates
249
+ end
250
+ end
251
+ ```
252
+
235
253
  ### Active Record is not required at all
236
254
 
237
255
  ```ruby
@@ -286,6 +304,26 @@ class ProductSearch
286
304
  end
287
305
  ```
288
306
 
307
+ ### Extracting basic module
308
+
309
+ You can extarct a basic search class for your application.
310
+
311
+ ```ruby
312
+ class BaseSearch
313
+ include SearchObject.module
314
+
315
+ # ... options and configuration
316
+ end
317
+ ```
318
+
319
+ Then use it like:
320
+
321
+ ```ruby
322
+ class ProductSearch < BaseSearch
323
+ scope { Product }
324
+ end
325
+ ```
326
+
289
327
  ## Contributing
290
328
 
291
329
  1. Fork it
data/example/Gemfile CHANGED
@@ -1,8 +1,8 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem 'rails', '4.0.0'
3
+ gem 'rails', '4.2.0'
4
4
  gem 'sqlite3'
5
- gem 'sass-rails', '~> 4.0.0'
5
+ gem 'sass-rails', '~> 5.0.0'
6
6
  gem 'slim'
7
7
  gem 'jquery-rails'
8
8
  gem 'bootstrap-sass'
@@ -4,6 +4,7 @@ html
4
4
  title SearchObject example
5
5
 
6
6
  = stylesheet_link_tag 'application', media: 'all'
7
+ = favicon_link_tag 'favicon.png'
7
8
  = csrf_meta_tags
8
9
 
9
10
  body
@@ -13,7 +13,7 @@ Example::Application.configure do
13
13
  config.eager_load = false
14
14
 
15
15
  # Configure static asset server for tests with Cache-Control for performance.
16
- config.serve_static_assets = true
16
+ config.serve_static_files = true
17
17
  config.static_cache_control = "public, max-age=3600"
18
18
 
19
19
  # Show full error reports and disable caching.
Binary file
@@ -1,3 +1,5 @@
1
+ # rubocop:disable Lint/UselessAssignment:%s
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe PostSearch do
@@ -13,7 +15,7 @@ describe PostSearch do
13
15
  title: 'Title',
14
16
  body: 'Body',
15
17
  category_name: 'Tech',
16
- published: true,
18
+ published: true
17
19
  )
18
20
  end
19
21
 
@@ -21,35 +23,35 @@ describe PostSearch do
21
23
  expect(PostSearch.new(filters: options, page: 0).results)
22
24
  end
23
25
 
24
- it "can search by category name" do
26
+ it 'can search by category name' do
25
27
  post = create category_name: 'Personal'
26
28
  other = create category_name: 'Other'
27
29
 
28
30
  expect_search(category_name: 'Personal').to eq [post]
29
31
  end
30
32
 
31
- it "can search by user_id" do
33
+ it 'can search by user_id' do
32
34
  post = create user: create_user
33
35
  other = create user: create_user
34
36
 
35
37
  expect_search(user_id: post.user_id).to eq [post]
36
38
  end
37
39
 
38
- it "can search by title" do
40
+ it 'can search by title' do
39
41
  post = create title: 'Title'
40
42
  other = create title: 'Other'
41
43
 
42
44
  expect_search(title: 'itl').to eq [post]
43
45
  end
44
46
 
45
- it "can search by published" do
47
+ it 'can search by published' do
46
48
  post = create published: true
47
49
  other = create published: false
48
50
 
49
51
  expect_search(published: true).to eq [post]
50
52
  end
51
53
 
52
- it "can search by term" do
54
+ it 'can search by term' do
53
55
  post_with_body = create body: 'pattern'
54
56
  post_with_title = create title: 'pattern'
55
57
  other = create
@@ -57,21 +59,21 @@ describe PostSearch do
57
59
  expect_search(term: 'pattern').to eq [post_with_title, post_with_body]
58
60
  end
59
61
 
60
- it "can search by created after" do
62
+ it 'can search by created after' do
61
63
  post = create created_at: 1.month.ago
62
64
  other = create created_at: 3.month.ago
63
65
 
64
66
  expect_search(created_after: 2.month.ago.strftime('%Y-%m-%d')).to eq [post]
65
67
  end
66
68
 
67
- it "can search by created before" do
69
+ it 'can search by created before' do
68
70
  post = create created_at: 3.month.ago
69
71
  other = create created_at: 1.month.ago
70
72
 
71
73
  expect_search(created_before: 2.month.ago.strftime('%Y-%m-%d')).to eq [post]
72
74
  end
73
75
 
74
- it "can sort by views count" do
76
+ it 'can sort by views count' do
75
77
  post_3 = create views_count: 3
76
78
  post_2 = create views_count: 2
77
79
  post_1 = create views_count: 1
@@ -1,9 +1,8 @@
1
1
  # This file is copied to spec/ when you run 'rails generate rspec:install'
2
2
  ENV['RAILS_ENV'] ||= 'test'
3
3
 
4
- require File.expand_path("../../config/environment", __FILE__)
4
+ require File.expand_path('../../config/environment', __FILE__)
5
5
  require 'rspec/rails'
6
- require 'rspec/autorun'
7
6
 
8
7
  # Requires supporting ruby files with custom matchers and macros, etc,
9
8
  # in spec/support/ and its subdirectories.
@@ -3,14 +3,16 @@ module SearchObject
3
3
  def self.included(base)
4
4
  base.extend ClassMethods
5
5
  base.instance_eval do
6
- @defaults = {}
7
- @actions = {}
8
- @scope = nil
6
+ @config = {
7
+ defaults: {},
8
+ actions: {},
9
+ scope: nil
10
+ }
9
11
  end
10
12
  end
11
13
 
12
14
  def initialize(options = {})
13
- @search = self.class.build_internal_search options
15
+ @search = Search.build_for self.class.config, options
14
16
  end
15
17
 
16
18
  def results
@@ -40,23 +42,29 @@ module SearchObject
40
42
  end
41
43
 
42
44
  module ClassMethods
43
- # :api: private
44
- def build_internal_search(options)
45
- scope = options.fetch(:scope) { @scope && @scope.call } or raise MissingScopeError
46
- params = @defaults.merge Helper.select_keys(Helper.stringify_keys(options.fetch(:filters, {})), @actions.keys)
45
+ attr_reader :config
47
46
 
48
- Search.new scope, params, @actions
47
+ def inherited(base)
48
+ new_config = config.dup
49
+
50
+ base.instance_eval do
51
+ @config = new_config
52
+ end
49
53
  end
50
54
 
51
55
  def scope(&block)
52
- @scope = block
56
+ config[:scope] = block
53
57
  end
54
58
 
55
- def option(name, default = nil, &block)
56
- name = name.to_s
59
+ def option(name, options = nil, &block)
60
+ options = { default: options } unless options.is_a?(Hash)
61
+
62
+ name = name.to_s
63
+ default = options[:default]
64
+ handler = options[:with] || block
57
65
 
58
- @defaults[name] = default unless default.nil?
59
- @actions[name] = block || ->(scope, value) { scope.where name => value unless value.blank? }
66
+ config[:defaults][name] = default unless default.nil?
67
+ config[:actions][name] = Helper.normalize_search_handler(handler, name)
60
68
 
61
69
  define_method(name) { @search.param name }
62
70
  end
@@ -2,18 +2,18 @@ module SearchObject
2
2
  module Helper
3
3
  class << self
4
4
  def stringify_keys(hash)
5
- Hash[(hash || {}).map { |k, v| [k.to_s, v]}]
5
+ Hash[(hash || {}).map { |k, v| [k.to_s, v] }]
6
6
  end
7
7
 
8
8
  def select_keys(hash, keys)
9
9
  keys.inject({}) do |memo, key|
10
- memo[key] = hash[key] if hash.has_key? key
10
+ memo[key] = hash[key] if hash.key? key
11
11
  memo
12
12
  end
13
13
  end
14
14
 
15
15
  def camelize(text)
16
- text.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
16
+ text.to_s.gsub(/(?:^|_)(.)/) { Regexp.last_match[1].upcase }
17
17
  end
18
18
 
19
19
  def ensure_included(item, collection)
@@ -27,10 +27,18 @@ module SearchObject
27
27
  def define_module(&block)
28
28
  Module.new do
29
29
  define_singleton_method :included do |base|
30
- base.class_eval &block
30
+ base.class_eval(&block)
31
31
  end
32
32
  end
33
33
  end
34
+
35
+ def normalize_search_handler(handler, name)
36
+ case handler
37
+ when Symbol then ->(scope, value) { method(handler).call scope, value }
38
+ when Proc then handler
39
+ else ->(scope, value) { scope.where name => value unless value.blank? }
40
+ end
41
+ end
34
42
  end
35
43
  end
36
44
  end
@@ -15,4 +15,3 @@ module SearchObject
15
15
  end
16
16
  end
17
17
  end
18
-
@@ -21,30 +21,30 @@ module SearchObject
21
21
  end
22
22
 
23
23
  def apply_paging(scope)
24
- scope.limit(per_page).offset ([page, 1].max - 1) * per_page
24
+ scope.limit(per_page).offset(([page, 1].max - 1) * per_page)
25
25
  end
26
26
 
27
27
  module ClassMethods
28
28
  def per_page(number)
29
- raise InvalidNumberError.new('Per page', number) unless number > 0
30
- @per_page = number
29
+ fail InvalidNumberError.new('Per page', number) unless number > 0
30
+ config[:per_page] = number
31
31
  end
32
32
 
33
33
  def min_per_page(number)
34
- raise InvalidNumberError.new('Min per page', number) unless number > 0
35
- @min_per_page = number
34
+ fail InvalidNumberError.new('Min per page', number) unless number > 0
35
+ config[:min_per_page] = number
36
36
  end
37
37
 
38
38
  def max_per_page(number)
39
- raise InvalidNumberError.new('Max per page', number) unless number > 0
40
- @max_per_page = number
39
+ fail InvalidNumberError.new('Max per page', number) unless number > 0
40
+ config[:max_per_page] = number
41
41
  end
42
42
 
43
43
  # :api: private
44
44
  def calculate_per_page(given)
45
- per_page = (given || @per_page || 25).to_i.abs
46
- per_page = [per_page, @max_per_page].min if @max_per_page
47
- per_page = [per_page, @min_per_page].max if @min_per_page
45
+ per_page = (given || config[:per_page] || 25).to_i.abs
46
+ per_page = [per_page, config[:max_per_page]].min if config[:max_per_page]
47
+ per_page = [per_page, config[:min_per_page]].max if config[:min_per_page]
48
48
  per_page
49
49
  end
50
50
  end
@@ -4,7 +4,7 @@ module SearchObject
4
4
  def self.included(base)
5
5
  base.extend ClassMethods
6
6
  base.instance_eval do
7
- option :sort do |scope, value|
7
+ option :sort do |scope, _|
8
8
  scope.order "#{sort_attribute} #{sort_direction}"
9
9
  end
10
10
  end
@@ -41,12 +41,12 @@ module SearchObject
41
41
 
42
42
  module ClassMethods
43
43
  def sort_by(*attributes)
44
- @sort_attributes = attributes.map(&:to_s)
45
- @defaults['sort'] = "#{@sort_attributes.first} desc"
44
+ config[:sort_attributes] = attributes.map(&:to_s)
45
+ config[:defaults]['sort'] = "#{config[:sort_attributes].first} desc"
46
46
  end
47
47
 
48
48
  def sort_attributes
49
- @sort_attributes ||= []
49
+ config[:sort_attributes] ||= []
50
50
  end
51
51
  end
52
52
  end
@@ -2,6 +2,18 @@ module SearchObject
2
2
  class Search
3
3
  attr_reader :params
4
4
 
5
+ class << self
6
+ def build_for(config, options)
7
+ scope = options.fetch(:scope) { config[:scope] && config[:scope].call }
8
+ filters = Helper.stringify_keys(options.fetch(:filters, {}))
9
+ params = config[:defaults].merge Helper.select_keys(filters, config[:actions].keys)
10
+
11
+ fail MissingScopeError unless scope
12
+
13
+ new scope, params, config[:actions]
14
+ end
15
+ end
16
+
5
17
  def initialize(scope, params, actions)
6
18
  @scope = scope
7
19
  @actions = actions
@@ -1,3 +1,3 @@
1
1
  module SearchObject
2
- VERSION = '1.0'
2
+ VERSION = '1.1.0'
3
3
  end
@@ -27,4 +27,6 @@ Gem::Specification.new do |spec|
27
27
  spec.add_development_dependency 'coveralls'
28
28
  spec.add_development_dependency 'will_paginate'
29
29
  spec.add_development_dependency 'kaminari'
30
+ spec.add_development_dependency 'rubocop'
31
+ spec.add_development_dependency 'rubocop-rspec'
30
32
  end