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