search_object 1.0 → 1.1.0

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