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.
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