search_object 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. data/.gitignore +17 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +8 -0
  4. data/CHANGELOG.md +5 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +283 -0
  8. data/Rakefile +6 -0
  9. data/example/.gitignore +16 -0
  10. data/example/.rspec +1 -0
  11. data/example/Gemfile +11 -0
  12. data/example/README.md +34 -0
  13. data/example/Rakefile +6 -0
  14. data/example/app/assets/javascripts/application.js +5 -0
  15. data/example/app/assets/stylesheets/application.css.scss +40 -0
  16. data/example/app/assets/stylesheets/reset.css +43 -0
  17. data/example/app/controllers/application_controller.rb +3 -0
  18. data/example/app/controllers/posts_controller.rb +5 -0
  19. data/example/app/models/.keep +0 -0
  20. data/example/app/models/post.rb +13 -0
  21. data/example/app/models/post_search.rb +44 -0
  22. data/example/app/models/user.rb +5 -0
  23. data/example/app/views/layouts/application.html.slim +12 -0
  24. data/example/app/views/posts/index.html.slim +48 -0
  25. data/example/bin/bundle +3 -0
  26. data/example/bin/rails +4 -0
  27. data/example/bin/rake +4 -0
  28. data/example/config.ru +4 -0
  29. data/example/config/application.rb +27 -0
  30. data/example/config/boot.rb +4 -0
  31. data/example/config/database.yml +12 -0
  32. data/example/config/environment.rb +5 -0
  33. data/example/config/environments/development.rb +29 -0
  34. data/example/config/environments/test.rb +37 -0
  35. data/example/config/initializers/filter_parameter_logging.rb +4 -0
  36. data/example/config/initializers/secret_token.rb +12 -0
  37. data/example/config/initializers/session_store.rb +3 -0
  38. data/example/config/initializers/wrap_parameters.rb +14 -0
  39. data/example/config/routes.rb +3 -0
  40. data/example/db/migrate/20131102130117_create_users.rb +10 -0
  41. data/example/db/migrate/20131102130413_create_posts.rb +18 -0
  42. data/example/db/schema.rb +40 -0
  43. data/example/db/seeds.rb +37 -0
  44. data/example/log/.keep +0 -0
  45. data/example/screenshot.png +0 -0
  46. data/example/spec/models/post_search_spec.rb +81 -0
  47. data/example/spec/spec_helper.rb +19 -0
  48. data/lib/search_object.rb +20 -0
  49. data/lib/search_object/base.rb +64 -0
  50. data/lib/search_object/helper.rb +36 -0
  51. data/lib/search_object/plugin/kaminari.rb +18 -0
  52. data/lib/search_object/plugin/model.rb +16 -0
  53. data/lib/search_object/plugin/paging.rb +42 -0
  54. data/lib/search_object/plugin/sorting.rb +54 -0
  55. data/lib/search_object/plugin/will_paginate.rb +17 -0
  56. data/lib/search_object/search.rb +26 -0
  57. data/lib/search_object/version.rb +3 -0
  58. data/search_object.gemspec +31 -0
  59. data/spec/search_object/base_spec.rb +237 -0
  60. data/spec/search_object/helper_spec.rb +30 -0
  61. data/spec/search_object/plugin/kaminari_spec.rb +50 -0
  62. data/spec/search_object/plugin/model_spec.rb +22 -0
  63. data/spec/search_object/plugin/paging_spec.rb +43 -0
  64. data/spec/search_object/plugin/sorting_spec.rb +139 -0
  65. data/spec/search_object/plugin/will_paginate_spec.rb +51 -0
  66. data/spec/search_object/search_spec.rb +72 -0
  67. data/spec/spec_helper.rb +13 -0
  68. data/spec/spec_helper_active_record.rb +19 -0
  69. data/spec/support/kaminari_setup.rb +7 -0
  70. metadata +292 -0
@@ -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 ''
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,16 @@
1
+ module SearchObject
2
+ module Plugin
3
+ module Model
4
+ def self.included(base)
5
+ base.class_eval do
6
+ include ActiveModel::Conversion
7
+ extend ActiveModel::Naming
8
+ end
9
+ end
10
+
11
+ def persisted?
12
+ false
13
+ end
14
+ end
15
+ end
16
+ end
@@ -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