search_object 0.1
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.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +283 -0
- data/Rakefile +6 -0
- data/example/.gitignore +16 -0
- data/example/.rspec +1 -0
- data/example/Gemfile +11 -0
- data/example/README.md +34 -0
- data/example/Rakefile +6 -0
- data/example/app/assets/javascripts/application.js +5 -0
- data/example/app/assets/stylesheets/application.css.scss +40 -0
- data/example/app/assets/stylesheets/reset.css +43 -0
- data/example/app/controllers/application_controller.rb +3 -0
- data/example/app/controllers/posts_controller.rb +5 -0
- data/example/app/models/.keep +0 -0
- data/example/app/models/post.rb +13 -0
- data/example/app/models/post_search.rb +44 -0
- data/example/app/models/user.rb +5 -0
- data/example/app/views/layouts/application.html.slim +12 -0
- data/example/app/views/posts/index.html.slim +48 -0
- data/example/bin/bundle +3 -0
- data/example/bin/rails +4 -0
- data/example/bin/rake +4 -0
- data/example/config.ru +4 -0
- data/example/config/application.rb +27 -0
- data/example/config/boot.rb +4 -0
- data/example/config/database.yml +12 -0
- data/example/config/environment.rb +5 -0
- data/example/config/environments/development.rb +29 -0
- data/example/config/environments/test.rb +37 -0
- data/example/config/initializers/filter_parameter_logging.rb +4 -0
- data/example/config/initializers/secret_token.rb +12 -0
- data/example/config/initializers/session_store.rb +3 -0
- data/example/config/initializers/wrap_parameters.rb +14 -0
- data/example/config/routes.rb +3 -0
- data/example/db/migrate/20131102130117_create_users.rb +10 -0
- data/example/db/migrate/20131102130413_create_posts.rb +18 -0
- data/example/db/schema.rb +40 -0
- data/example/db/seeds.rb +37 -0
- data/example/log/.keep +0 -0
- data/example/screenshot.png +0 -0
- data/example/spec/models/post_search_spec.rb +81 -0
- data/example/spec/spec_helper.rb +19 -0
- data/lib/search_object.rb +20 -0
- data/lib/search_object/base.rb +64 -0
- data/lib/search_object/helper.rb +36 -0
- data/lib/search_object/plugin/kaminari.rb +18 -0
- data/lib/search_object/plugin/model.rb +16 -0
- data/lib/search_object/plugin/paging.rb +42 -0
- data/lib/search_object/plugin/sorting.rb +54 -0
- data/lib/search_object/plugin/will_paginate.rb +17 -0
- data/lib/search_object/search.rb +26 -0
- data/lib/search_object/version.rb +3 -0
- data/search_object.gemspec +31 -0
- data/spec/search_object/base_spec.rb +237 -0
- data/spec/search_object/helper_spec.rb +30 -0
- data/spec/search_object/plugin/kaminari_spec.rb +50 -0
- data/spec/search_object/plugin/model_spec.rb +22 -0
- data/spec/search_object/plugin/paging_spec.rb +43 -0
- data/spec/search_object/plugin/sorting_spec.rb +139 -0
- data/spec/search_object/plugin/will_paginate_spec.rb +51 -0
- data/spec/search_object/search_spec.rb +72 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/spec_helper_active_record.rb +19 -0
- data/spec/support/kaminari_setup.rb +7 -0
- metadata +292 -0
data/example/db/seeds.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
User.delete_all
|
2
|
+
Post.delete_all
|
3
|
+
|
4
|
+
users = [
|
5
|
+
User.create!(name: 'John'),
|
6
|
+
User.create!(name: 'Jake'),
|
7
|
+
User.create!(name: 'Jade'),
|
8
|
+
]
|
9
|
+
|
10
|
+
category_names = [
|
11
|
+
'Books',
|
12
|
+
'Code',
|
13
|
+
'Design',
|
14
|
+
'Database',
|
15
|
+
'Education',
|
16
|
+
'Personal',
|
17
|
+
'News',
|
18
|
+
'Stuff',
|
19
|
+
'Others'
|
20
|
+
]
|
21
|
+
|
22
|
+
400.times do |i|
|
23
|
+
Post.create!(
|
24
|
+
user: users.sample,
|
25
|
+
category_name: category_names.sample,
|
26
|
+
title: "Example post #{i + 1}",
|
27
|
+
body: 'Body text',
|
28
|
+
views_count: rand(1000),
|
29
|
+
likes_count: rand(1000),
|
30
|
+
comments_count: rand(1000),
|
31
|
+
published: [true, false].sample,
|
32
|
+
created_at: rand(30).days.ago
|
33
|
+
)
|
34
|
+
print '.'
|
35
|
+
end
|
36
|
+
|
37
|
+
puts ''
|
data/example/log/.keep
ADDED
File without changes
|
Binary file
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe PostSearch do
|
4
|
+
let(:user) { create_user }
|
5
|
+
|
6
|
+
def create_user
|
7
|
+
User.create! name: "User #{User.count + 1}"
|
8
|
+
end
|
9
|
+
|
10
|
+
def create(attributes = {})
|
11
|
+
Post.create! attributes.reverse_merge(
|
12
|
+
user: user,
|
13
|
+
title: 'Title',
|
14
|
+
body: 'Body',
|
15
|
+
category_name: 'Tech',
|
16
|
+
published: true,
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
def expect_search(options)
|
21
|
+
expect(PostSearch.new(options, 0).results)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "can search by category name" do
|
25
|
+
post = create category_name: 'Personal'
|
26
|
+
other = create category_name: 'Other'
|
27
|
+
|
28
|
+
expect_search(category_name: 'Personal').to eq [post]
|
29
|
+
end
|
30
|
+
|
31
|
+
it "can search by user_id" do
|
32
|
+
post = create user: create_user
|
33
|
+
other = create user: create_user
|
34
|
+
|
35
|
+
expect_search(user_id: post.user_id).to eq [post]
|
36
|
+
end
|
37
|
+
|
38
|
+
it "can search by title" do
|
39
|
+
post = create title: 'Title'
|
40
|
+
other = create title: 'Other'
|
41
|
+
|
42
|
+
expect_search(title: 'itl').to eq [post]
|
43
|
+
end
|
44
|
+
|
45
|
+
it "can search by published" do
|
46
|
+
post = create published: true
|
47
|
+
other = create published: false
|
48
|
+
|
49
|
+
expect_search(published: true).to eq [post]
|
50
|
+
end
|
51
|
+
|
52
|
+
it "can search by term" do
|
53
|
+
post_with_body = create body: 'pattern'
|
54
|
+
post_with_title = create title: 'pattern'
|
55
|
+
other = create
|
56
|
+
|
57
|
+
expect_search(term: 'pattern').to eq [post_with_title, post_with_body]
|
58
|
+
end
|
59
|
+
|
60
|
+
it "can search by created after" do
|
61
|
+
post = create created_at: 1.month.ago
|
62
|
+
other = create created_at: 3.month.ago
|
63
|
+
|
64
|
+
expect_search(created_after: 2.month.ago.strftime('%Y-%m-%d')).to eq [post]
|
65
|
+
end
|
66
|
+
|
67
|
+
it "can search by created before" do
|
68
|
+
post = create created_at: 3.month.ago
|
69
|
+
other = create created_at: 1.month.ago
|
70
|
+
|
71
|
+
expect_search(created_before: 2.month.ago.strftime('%Y-%m-%d')).to eq [post]
|
72
|
+
end
|
73
|
+
|
74
|
+
it "can sort by views count" do
|
75
|
+
post_3 = create views_count: 3
|
76
|
+
post_2 = create views_count: 2
|
77
|
+
post_1 = create views_count: 1
|
78
|
+
|
79
|
+
expect_search(sort: 'views_count').to eq [post_3, post_2, post_1]
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# This file is copied to spec/ when you run 'rails generate rspec:install'
|
2
|
+
ENV['RAILS_ENV'] ||= 'test'
|
3
|
+
|
4
|
+
require File.expand_path("../../config/environment", __FILE__)
|
5
|
+
require 'rspec/rails'
|
6
|
+
require 'rspec/autorun'
|
7
|
+
|
8
|
+
# Requires supporting ruby files with custom matchers and macros, etc,
|
9
|
+
# in spec/support/ and its subdirectories.
|
10
|
+
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
|
11
|
+
|
12
|
+
# Checks for pending migrations before tests are run.
|
13
|
+
# If you are not using ActiveRecord, you can remove this line.
|
14
|
+
ActiveRecord::Migration.check_pending! if defined?(ActiveRecord::Migration)
|
15
|
+
|
16
|
+
RSpec.configure do |config|
|
17
|
+
config.use_transactional_fixtures = true
|
18
|
+
config.infer_base_class_for_anonymous_controllers = false
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'search_object/version'
|
2
|
+
require 'search_object/helper'
|
3
|
+
require 'search_object/base'
|
4
|
+
require 'search_object/search'
|
5
|
+
require 'search_object/plugin/model'
|
6
|
+
require 'search_object/plugin/paging'
|
7
|
+
require 'search_object/plugin/will_paginate'
|
8
|
+
require 'search_object/plugin/kaminari'
|
9
|
+
require 'search_object/plugin/sorting'
|
10
|
+
|
11
|
+
module SearchObject
|
12
|
+
def self.module(*plugins)
|
13
|
+
return Base if plugins.empty?
|
14
|
+
|
15
|
+
Helper.define_module do
|
16
|
+
include Base
|
17
|
+
plugins.each { |plugin_name| include Plugin.const_get(Helper.camelize(plugin_name)) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module SearchObject
|
2
|
+
module Base
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
base.instance_eval do
|
6
|
+
@defaults = {}
|
7
|
+
@actions = {}
|
8
|
+
@scope = nil
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(*args)
|
13
|
+
@search = self.class.search args
|
14
|
+
end
|
15
|
+
|
16
|
+
def results
|
17
|
+
@results ||= fetch_results
|
18
|
+
end
|
19
|
+
|
20
|
+
def results?
|
21
|
+
results.any?
|
22
|
+
end
|
23
|
+
|
24
|
+
def count
|
25
|
+
@count ||= @search.count self
|
26
|
+
end
|
27
|
+
|
28
|
+
def params(additions = {})
|
29
|
+
if additions.empty?
|
30
|
+
@search.params
|
31
|
+
else
|
32
|
+
@search.params.merge Helper.stringify_keys(additions)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def fetch_results
|
39
|
+
@search.query self
|
40
|
+
end
|
41
|
+
|
42
|
+
module ClassMethods
|
43
|
+
def search(args)
|
44
|
+
scope = (@scope && @scope.call) || args.shift
|
45
|
+
params = @defaults.merge(Helper.select_keys Helper.stringify_keys(args.shift || {}), @actions.keys)
|
46
|
+
|
47
|
+
Search.new scope, params, @actions
|
48
|
+
end
|
49
|
+
|
50
|
+
def scope(&block)
|
51
|
+
@scope = block
|
52
|
+
end
|
53
|
+
|
54
|
+
def option(name, default = nil, &block)
|
55
|
+
name = name.to_s
|
56
|
+
|
57
|
+
@defaults[name] = default unless default.nil?
|
58
|
+
@actions[name] = block || ->(scope, value) { scope.where name => value unless value.blank? }
|
59
|
+
|
60
|
+
define_method(name) { @search.param name }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module SearchObject
|
2
|
+
module Helper
|
3
|
+
class << self
|
4
|
+
def stringify_keys(hash)
|
5
|
+
Hash[(hash || {}).map { |k, v| [k.to_s, v]}]
|
6
|
+
end
|
7
|
+
|
8
|
+
def select_keys(hash, keys)
|
9
|
+
keys.inject({}) do |memo, key|
|
10
|
+
memo[key] = hash[key] if hash.has_key? key
|
11
|
+
memo
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def camelize(text)
|
16
|
+
text.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
|
17
|
+
end
|
18
|
+
|
19
|
+
def ensure_included(item, collection)
|
20
|
+
if collection.include? item
|
21
|
+
item
|
22
|
+
else
|
23
|
+
collection.first
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def define_module(&block)
|
28
|
+
Module.new do
|
29
|
+
define_singleton_method :included do |base|
|
30
|
+
base.class_eval &block
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module SearchObject
|
2
|
+
module Plugin
|
3
|
+
module Kaminari
|
4
|
+
include Paging
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.extend Paging::ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def apply_paging(scope)
|
13
|
+
scope.page(page).per(per_page)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module SearchObject
|
2
|
+
module Plugin
|
3
|
+
module Paging
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(*args)
|
9
|
+
@page = args.pop.to_i.abs
|
10
|
+
super *args
|
11
|
+
end
|
12
|
+
|
13
|
+
def page
|
14
|
+
@page
|
15
|
+
end
|
16
|
+
|
17
|
+
def per_page
|
18
|
+
self.class.get_per_page
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def fetch_results
|
24
|
+
apply_paging super
|
25
|
+
end
|
26
|
+
|
27
|
+
def apply_paging(scope)
|
28
|
+
scope.limit(page * per_page).offset(per_page)
|
29
|
+
end
|
30
|
+
|
31
|
+
module ClassMethods
|
32
|
+
def per_page(number)
|
33
|
+
@per_page = number.to_i.abs
|
34
|
+
end
|
35
|
+
|
36
|
+
def get_per_page
|
37
|
+
@per_page ||= 25
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module SearchObject
|
2
|
+
module Plugin
|
3
|
+
module Sorting
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
base.instance_eval do
|
7
|
+
option :sort do |scope, value|
|
8
|
+
scope.order "#{sort_attribute} #{sort_direction}"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def sort?(attribute)
|
14
|
+
attribute == sort || sort.to_s.starts_with?("#{attribute} ")
|
15
|
+
end
|
16
|
+
|
17
|
+
def sort_attribute
|
18
|
+
@sort_attribute ||= Helper.ensure_included sort.to_s.split(' ', 2).first, self.class.sort_attributes
|
19
|
+
end
|
20
|
+
|
21
|
+
def sort_direction
|
22
|
+
@sort_direction ||= Helper.ensure_included sort.to_s.split(' ', 2).last, %w(desc asc)
|
23
|
+
end
|
24
|
+
|
25
|
+
def sort_direction_for(attribute)
|
26
|
+
if sort_attribute == attribute.to_s
|
27
|
+
reverted_sort_direction
|
28
|
+
else
|
29
|
+
'desc'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def sort_params_for(attribute, options = {})
|
34
|
+
options['sort'] = "#{attribute} #{sort_direction_for(attribute)}"
|
35
|
+
params options
|
36
|
+
end
|
37
|
+
|
38
|
+
def reverted_sort_direction
|
39
|
+
sort_direction == 'desc' ? 'asc' : 'desc'
|
40
|
+
end
|
41
|
+
|
42
|
+
module ClassMethods
|
43
|
+
def sort_by(*attributes)
|
44
|
+
@sort_attributes = attributes.map(&:to_s)
|
45
|
+
@defaults['sort'] = "#{@sort_attributes.first} desc"
|
46
|
+
end
|
47
|
+
|
48
|
+
def sort_attributes
|
49
|
+
@sort_attributes ||= []
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module SearchObject
|
2
|
+
module Plugin
|
3
|
+
module WillPaginate
|
4
|
+
include Paging
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.extend Paging::ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def apply_paging(scope)
|
13
|
+
scope.paginate per_page: per_page, page: page == 0 ? nil : page
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module SearchObject
|
2
|
+
class Search
|
3
|
+
attr_reader :params
|
4
|
+
|
5
|
+
def initialize(scope, params, actions)
|
6
|
+
@scope = scope
|
7
|
+
@actions = actions
|
8
|
+
@params = params
|
9
|
+
end
|
10
|
+
|
11
|
+
def param(name)
|
12
|
+
@params[name]
|
13
|
+
end
|
14
|
+
|
15
|
+
def query(context)
|
16
|
+
@params.inject(@scope) do |scope, (name, value)|
|
17
|
+
new_scope = context.instance_exec scope, value, &@actions[name]
|
18
|
+
new_scope || scope
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def count(context)
|
23
|
+
query(context).count
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|