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 +4 -4
- data/.rubocop.yml +24 -0
- data/.travis.yml +4 -2
- data/CHANGELOG.md +40 -0
- data/README.md +38 -0
- data/example/Gemfile +2 -2
- data/example/app/assets/images/favicon.png +0 -0
- data/example/app/views/layouts/application.html.slim +1 -0
- data/example/config/environments/test.rb +1 -1
- data/example/screenshot.png +0 -0
- data/example/spec/models/post_search_spec.rb +11 -9
- data/example/spec/spec_helper.rb +1 -2
- data/lib/search_object/base.rb +22 -14
- data/lib/search_object/helper.rb +12 -4
- data/lib/search_object/plugin/kaminari.rb +0 -1
- data/lib/search_object/plugin/paging.rb +10 -10
- data/lib/search_object/plugin/sorting.rb +4 -4
- data/lib/search_object/search.rb +12 -0
- data/lib/search_object/version.rb +1 -1
- data/search_object.gemspec +2 -0
- data/spec/search_object/base_spec.rb +93 -57
- data/spec/search_object/helper_spec.rb +7 -7
- data/spec/search_object/plugin/kaminari_spec.rb +3 -3
- data/spec/search_object/plugin/model_spec.rb +6 -2
- data/spec/search_object/plugin/paging_spec.rb +1 -1
- data/spec/search_object/plugin/sorting_spec.rb +38 -33
- data/spec/search_object/plugin/will_paginate_spec.rb +3 -3
- data/spec/search_object/search_spec.rb +25 -26
- data/spec/spec_helper.rb +0 -1
- data/spec/spec_helper_active_record.rb +1 -1
- data/spec/support/kaminari_setup.rb +0 -1
- data/spec/support/paging_shared_example.rb +35 -32
- metadata +56 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 24b14c3e7ec2492dc899f0a81a022089073be982
|
4
|
+
data.tar.gz: 8bee82cac11549acebcc9d35cc9cbb2aac25f456
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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
Binary file
|
@@ -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.
|
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.
|
data/example/screenshot.png
CHANGED
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
data/example/spec/spec_helper.rb
CHANGED
@@ -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(
|
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.
|
data/lib/search_object/base.rb
CHANGED
@@ -3,14 +3,16 @@ module SearchObject
|
|
3
3
|
def self.included(base)
|
4
4
|
base.extend ClassMethods
|
5
5
|
base.instance_eval do
|
6
|
-
@
|
7
|
-
|
8
|
-
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
56
|
+
config[:scope] = block
|
53
57
|
end
|
54
58
|
|
55
|
-
def option(name,
|
56
|
-
|
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
|
-
|
59
|
-
|
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
|
data/lib/search_object/helper.rb
CHANGED
@@ -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.
|
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(
|
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
|
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
|
@@ -21,30 +21,30 @@ module SearchObject
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def apply_paging(scope)
|
24
|
-
scope.limit(per_page).offset
|
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
|
-
|
30
|
-
|
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
|
-
|
35
|
-
|
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
|
-
|
40
|
-
|
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 ||
|
46
|
-
per_page = [per_page,
|
47
|
-
per_page = [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,
|
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
|
-
|
45
|
-
|
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
|
-
|
49
|
+
config[:sort_attributes] ||= []
|
50
50
|
end
|
51
51
|
end
|
52
52
|
end
|
data/lib/search_object/search.rb
CHANGED
@@ -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
|
data/search_object.gemspec
CHANGED
@@ -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
|