trailblazer-finder 0.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.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rubocop.yml +45 -0
  4. data/.rubocop_todo.yml +52 -0
  5. data/.travis.yml +15 -0
  6. data/Gemfile +12 -0
  7. data/LICENSE.txt +23 -0
  8. data/README.md +494 -0
  9. data/Rakefile +29 -0
  10. data/lib/trailblazer/finder/adapters/active_record/paging.rb +20 -0
  11. data/lib/trailblazer/finder/adapters/active_record/sorting.rb +20 -0
  12. data/lib/trailblazer/finder/adapters/active_record.rb +30 -0
  13. data/lib/trailblazer/finder/adapters/data_mapper/paging.rb +20 -0
  14. data/lib/trailblazer/finder/adapters/data_mapper/sorting.rb +25 -0
  15. data/lib/trailblazer/finder/adapters/data_mapper.rb +30 -0
  16. data/lib/trailblazer/finder/adapters/friendly_id.rb +33 -0
  17. data/lib/trailblazer/finder/adapters/kaminari.rb +18 -0
  18. data/lib/trailblazer/finder/adapters/sequel/paging.rb +20 -0
  19. data/lib/trailblazer/finder/adapters/sequel/sorting.rb +25 -0
  20. data/lib/trailblazer/finder/adapters/sequel.rb +30 -0
  21. data/lib/trailblazer/finder/adapters/will_paginate.rb +18 -0
  22. data/lib/trailblazer/finder/adapters.rb +26 -0
  23. data/lib/trailblazer/finder/base.rb +98 -0
  24. data/lib/trailblazer/finder/errors/block_ignored.rb +11 -0
  25. data/lib/trailblazer/finder/errors/invalid_defined_by_value.rb +11 -0
  26. data/lib/trailblazer/finder/errors/invalid_number.rb +16 -0
  27. data/lib/trailblazer/finder/errors/missing_entity_type.rb +11 -0
  28. data/lib/trailblazer/finder/errors/with_ignored.rb +11 -0
  29. data/lib/trailblazer/finder/features/paging.rb +55 -0
  30. data/lib/trailblazer/finder/features/sorting.rb +66 -0
  31. data/lib/trailblazer/finder/features.rb +22 -0
  32. data/lib/trailblazer/finder/filter.rb +66 -0
  33. data/lib/trailblazer/finder/find.rb +29 -0
  34. data/lib/trailblazer/finder/utils/extra.rb +31 -0
  35. data/lib/trailblazer/finder/utils/params.rb +28 -0
  36. data/lib/trailblazer/finder/utils/parse.rb +25 -0
  37. data/lib/trailblazer/finder/utils/string.rb +35 -0
  38. data/lib/trailblazer/finder/version.rb +5 -0
  39. data/lib/trailblazer/finder.rb +29 -0
  40. data/lib/trailblazer/operation/finder.rb +61 -0
  41. data/spec/spec_helper.rb +15 -0
  42. data/spec/spec_helper_active_record.rb +50 -0
  43. data/spec/spec_helper_data_mapper.rb +35 -0
  44. data/spec/spec_helper_sequel.rb +32 -0
  45. data/spec/support/paging_shared_example.rb +65 -0
  46. data/spec/support/sorting_shared_example.rb +95 -0
  47. data/spec/trailblazer/finder/adapters/active_record/base_spec.rb +112 -0
  48. data/spec/trailblazer/finder/adapters/active_record/paging_spec.rb +64 -0
  49. data/spec/trailblazer/finder/adapters/active_record/sorting_spec.rb +82 -0
  50. data/spec/trailblazer/finder/adapters/data_mapper/base_spec.rb +112 -0
  51. data/spec/trailblazer/finder/adapters/data_mapper/paging_spec.rb +64 -0
  52. data/spec/trailblazer/finder/adapters/data_mapper/sorting_spec.rb +85 -0
  53. data/spec/trailblazer/finder/adapters/friendly_id_spec.rb +46 -0
  54. data/spec/trailblazer/finder/adapters/kaminari_spec.rb +64 -0
  55. data/spec/trailblazer/finder/adapters/sequel/base_spec.rb +112 -0
  56. data/spec/trailblazer/finder/adapters/sequel/paging_spec.rb +64 -0
  57. data/spec/trailblazer/finder/adapters/sequel/sorting_spec.rb +82 -0
  58. data/spec/trailblazer/finder/adapters/will_paginate_spec.rb +71 -0
  59. data/spec/trailblazer/finder/adapters_spec.rb +110 -0
  60. data/spec/trailblazer/finder/base_spec.rb +329 -0
  61. data/spec/trailblazer/finder/features/paging_spec.rb +104 -0
  62. data/spec/trailblazer/finder/features/sorting_spec.rb +100 -0
  63. data/spec/trailblazer/finder/features_spec.rb +55 -0
  64. data/spec/trailblazer/finder/filter_spec.rb +133 -0
  65. data/spec/trailblazer/finder/find_spec.rb +72 -0
  66. data/spec/trailblazer/finder/utils/extra_spec.rb +41 -0
  67. data/spec/trailblazer/finder/utils/params_spec.rb +39 -0
  68. data/spec/trailblazer/finder/utils/parse_spec.rb +33 -0
  69. data/spec/trailblazer/finder/utils/string_spec.rb +25 -0
  70. data/spec/trailblazer/operation/finder_spec.rb +103 -0
  71. data/spec/trailblazer/operation/paging_spec.rb +68 -0
  72. data/spec/trailblazer/operation/sorting_spec.rb +80 -0
  73. data/trailblazer-finder.gemspec +41 -0
  74. metadata +402 -0
data/Rakefile ADDED
@@ -0,0 +1,29 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+ require 'rubocop/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec) do |t|
6
+ t.rspec_opts = '--format documentation --format RspecJunitFormatter --out test-reports/spec.xml'
7
+ end
8
+ RSpec::Core::RakeTask.new(:tests) do |t|
9
+ t.rspec_opts = '--format progress --format documentation'
10
+ end
11
+ RSpec::Core::RakeTask.new(:spec_report) do |t|
12
+ t.rspec_opts = '--format html --out reports/rspec_results.html'
13
+ end
14
+
15
+ RuboCop::RakeTask.new(:rubocop)
16
+
17
+ desc 'Remove temporary files'
18
+ task :clean do
19
+ %x{rm -rf *.gem doc pkg coverage test-reports}
20
+ %x{rm -f `find . -name '*.rbc'`}
21
+ end
22
+
23
+ desc "Build the gem"
24
+ task :gem do
25
+ %x{gem build trailblazer-finder.gemspec}
26
+ end
27
+
28
+ desc 'Running Tests'
29
+ task default: %i[clean spec rubocop]
@@ -0,0 +1,20 @@
1
+ module Trailblazer
2
+ class Finder
3
+ module Adapters
4
+ module ActiveRecord
5
+ # ActiveRecord - Paging Adapter
6
+ module Paging
7
+ def self.included(base)
8
+ base.extend Features::Paging::ClassMethods
9
+ end
10
+
11
+ private
12
+
13
+ def apply_paging(entity_type)
14
+ entity_type.limit(per_page).offset(([page, 1].max - 1) * per_page)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module Trailblazer
2
+ class Finder
3
+ module Adapters
4
+ module ActiveRecord
5
+ # ActiveRecord - Sorting Adapter
6
+ module Sorting
7
+ def self.included(base)
8
+ base.extend Features::Sorting::ClassMethods
9
+ end
10
+
11
+ private
12
+
13
+ def sort_it(entity_type, sort_attribute, sort_direction)
14
+ entity_type.order sort_attribute.to_s => sort_direction
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,30 @@
1
+ require 'trailblazer/finder/adapters/active_record/paging'
2
+ require 'trailblazer/finder/adapters/active_record/sorting'
3
+
4
+ module Trailblazer
5
+ class Finder
6
+ module Adapters
7
+ # ActiveRecord Adapter
8
+ module ActiveRecord
9
+ def self.included(base)
10
+ base.extend ClassMethods
11
+ end
12
+
13
+ module ClassMethods
14
+ # :nocov:
15
+ def normalize_find_handler(handler, key)
16
+ case handler
17
+ when Symbol then ->(entity_type, value) { method(handler).call entity_type, value }
18
+ when Proc then handler
19
+ else ->(entity_type, value) { entity_type.where(key.to_sym => value) unless Utils::String.blank?(value) }
20
+ end
21
+ end
22
+ # :nocov:
23
+ end
24
+
25
+ include Paging if defined?(Features::Paging::ClassMethods)
26
+ include Sorting if defined?(Features::Sorting::ClassMethods)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,20 @@
1
+ module Trailblazer
2
+ class Finder
3
+ module Adapters
4
+ module DataMapper
5
+ # Sequel - Paging Adapter
6
+ module Paging
7
+ def self.included(base)
8
+ base.extend Features::Paging::ClassMethods
9
+ end
10
+
11
+ private
12
+
13
+ def apply_paging(entity_type)
14
+ entity_type.all(limit: per_page, offset: ([page, 1].max - 1) * per_page)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ module Trailblazer
2
+ class Finder
3
+ module Adapters
4
+ module DataMapper
5
+ # Sequel - Sorting Adapter
6
+ module Sorting
7
+ def self.included(base)
8
+ base.extend Features::Sorting::ClassMethods
9
+ end
10
+
11
+ private
12
+
13
+ def sort_it(entity_type, sort_attribute, sort_direction)
14
+ case sort_direction
15
+ when 'asc', 'ascending'
16
+ entity_type.all(order: [sort_attribute.to_sym.asc])
17
+ when 'desc', 'descending'
18
+ entity_type.all(order: [sort_attribute.to_sym.desc])
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,30 @@
1
+ require 'trailblazer/finder/adapters/data_mapper/paging'
2
+ require 'trailblazer/finder/adapters/data_mapper/sorting'
3
+
4
+ module Trailblazer
5
+ class Finder
6
+ module Adapters
7
+ # ActiveRecord Adapter
8
+ module DataMapper
9
+ def self.included(base)
10
+ base.extend ClassMethods
11
+ end
12
+
13
+ module ClassMethods
14
+ # :nocov:
15
+ def normalize_find_handler(handler, key)
16
+ case handler
17
+ when Symbol then ->(entity_type, value) { method(handler).call entity_type, value }
18
+ when Proc then handler
19
+ else ->(entity_type, value) { entity_type.all(key.to_sym => value) unless Utils::String.blank?(value) }
20
+ end
21
+ end
22
+ # :nocov:
23
+ end
24
+
25
+ include Paging if defined?(Features::Paging::ClassMethods)
26
+ include Sorting if defined?(Features::Sorting::ClassMethods)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,33 @@
1
+ if Gem.loaded_specs.key?('activerecord')
2
+ module Trailblazer
3
+ class Finder
4
+ module Adapters
5
+ # FriendlyId Adapter
6
+ module FriendlyId
7
+ def self.included(base)
8
+ base.extend ClassMethods
9
+ base.class_eval do
10
+ filter_by :id, with: :apply_slug_filter
11
+ end
12
+ end
13
+
14
+ def apply_slug_filter(entity_type, value)
15
+ return if value.nil?
16
+ return if value == ''
17
+ if num?(value)
18
+ entity_type.where(id: value)
19
+ else
20
+ entity_type.where(slug: Utils::String.underscore(value.downcase))
21
+ end
22
+ end
23
+
24
+ def num?(str)
25
+ Integer(str)
26
+ rescue ArgumentError, TypeError
27
+ false
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,18 @@
1
+ module Trailblazer
2
+ class Finder
3
+ module Adapters
4
+ # Kaminari Adapter
5
+ module Kaminari
6
+ def self.included(base)
7
+ base.extend Features::Paging::ClassMethods
8
+ end
9
+
10
+ private
11
+
12
+ def apply_paging(entity_type)
13
+ entity_type.page(page).per(per_page)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ module Trailblazer
2
+ class Finder
3
+ module Adapters
4
+ module Sequel
5
+ # Sequel - Paging Adapter
6
+ module Paging
7
+ def self.included(base)
8
+ base.extend Features::Paging::ClassMethods
9
+ end
10
+
11
+ private
12
+
13
+ def apply_paging(entity_type)
14
+ entity_type.limit(per_page).offset(([page, 1].max - 1) * per_page)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ module Trailblazer
2
+ class Finder
3
+ module Adapters
4
+ module Sequel
5
+ # Sequel - Sorting Adapter
6
+ module Sorting
7
+ def self.included(base)
8
+ base.extend Features::Sorting::ClassMethods
9
+ end
10
+
11
+ private
12
+
13
+ def sort_it(entity_type, sort_attribute, sort_direction)
14
+ case sort_direction
15
+ when 'asc', 'ascending'
16
+ entity_type.order(sort_attribute.to_sym)
17
+ when 'desc', 'descending'
18
+ entity_type.reverse(sort_attribute.to_sym)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,30 @@
1
+ require 'trailblazer/finder/adapters/sequel/paging'
2
+ require 'trailblazer/finder/adapters/sequel/sorting'
3
+
4
+ module Trailblazer
5
+ class Finder
6
+ module Adapters
7
+ # Sequel Adapter
8
+ module Sequel
9
+ def self.included(base)
10
+ base.extend ClassMethods
11
+ end
12
+
13
+ module ClassMethods
14
+ # :nocov:
15
+ def normalize_find_handler(handler, key)
16
+ case handler
17
+ when Symbol then ->(entity_type, value) { method(handler).call entity_type, value }
18
+ when Proc then handler
19
+ else ->(entity_type, value) { entity_type.where(key.to_sym => value) unless Utils::String.blank?(value) }
20
+ end
21
+ end
22
+ # :nocov:
23
+ end
24
+
25
+ include Paging if defined?(Features::Paging::ClassMethods)
26
+ include Sorting if defined?(Features::Sorting::ClassMethods)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,18 @@
1
+ module Trailblazer
2
+ class Finder
3
+ module Adapters
4
+ # WillPaginate Adapter
5
+ module WillPaginate
6
+ def self.included(base)
7
+ base.extend Features::Paging::ClassMethods
8
+ end
9
+
10
+ private
11
+
12
+ def apply_paging(entity_type)
13
+ entity_type.paginate per_page: per_page, page: page.zero? ? nil : page
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,26 @@
1
+ require 'trailblazer/finder/adapters/active_record'
2
+ require 'trailblazer/finder/adapters/sequel'
3
+ require 'trailblazer/finder/adapters/data_mapper'
4
+ require 'trailblazer/finder/adapters/kaminari'
5
+ require 'trailblazer/finder/adapters/will_paginate'
6
+ require 'trailblazer/finder/adapters/friendly_id'
7
+
8
+ module Trailblazer
9
+ class Finder
10
+ # Adapters
11
+ module Adapters
12
+ def self.included(base)
13
+ base.extend ClassMethods
14
+ end
15
+
16
+ # ClassMethods
17
+ module ClassMethods
18
+ def adapters(*mods)
19
+ mods.each do |mod|
20
+ include mod
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,98 @@
1
+ require 'hashie/extensions/deep_locate'
2
+
3
+ module Trailblazer
4
+ class Finder
5
+ # Base module
6
+ module Base
7
+ def self.included(base)
8
+ base.extend ClassMethods
9
+ base.instance_eval do
10
+ @config = {
11
+ defaults: {},
12
+ actions: {},
13
+ entity_type: nil
14
+ }
15
+ end
16
+ end
17
+
18
+ def initialize(options = {})
19
+ config = self.class.config
20
+ entity_type = options[:entity_type] || (config[:entity_type] && instance_eval(&config[:entity_type]))
21
+ actions = config[:actions] || {}
22
+ params = Utils::Params.normalize_params(config[:defaults], options[:filter], actions.keys)
23
+
24
+ raise Error::MissingEntityType unless entity_type
25
+
26
+ @find = Find.new(entity_type, params, actions)
27
+ end
28
+
29
+ def results
30
+ @results ||= fetch_results
31
+ end
32
+
33
+ def results?
34
+ results.any?
35
+ end
36
+
37
+ def count
38
+ @count ||= @find.count self
39
+ end
40
+
41
+ def params(additions = {})
42
+ if additions.empty?
43
+ @find.params
44
+ else
45
+ @find.params.merge Utils::Params.stringify_keys(additions)
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def fetch_results
52
+ @find.query self
53
+ end
54
+
55
+ # ClassMethods
56
+ module ClassMethods
57
+ attr_reader :config
58
+
59
+ def inherited(base)
60
+ base.instance_variable_set '@config', Utils::Extra.deep_copy(config)
61
+ end
62
+
63
+ def entity_type(&block)
64
+ config[:entity_type] = block
65
+ end
66
+
67
+ def filter_by(name, options = nil, &block)
68
+ options = { default: options } unless options.is_a?(Hash)
69
+
70
+ name = name.to_s
71
+ default = options[:default]
72
+ handler = options[:with] || block
73
+
74
+ config[:defaults][name] = default unless default.nil?
75
+ config[:actions][name] = normalize_find_handler(handler, name)
76
+
77
+ define_method(name) { @find.param name }
78
+ end
79
+
80
+ def results(*args)
81
+ new(*args).results
82
+ end
83
+
84
+ def normalize_find_handler(handler, key)
85
+ case handler
86
+ when Symbol then ->(entity_type, value) { method(handler).call entity_type, value }
87
+ when Proc then handler
88
+ else
89
+ lambda do |entity_type, value|
90
+ return if Utils::String.blank?(value)
91
+ Hashie::Extensions::DeepLocate.deep_locate ->(k, v, _object) { k == key.to_sym && v == value }, entity_type
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,11 @@
1
+ module Trailblazer
2
+ class Finder
3
+ module Error
4
+ class BlockIgnored < ArgumentError
5
+ def initialize(message = "defined_by filter_by's don't accept blocks")
6
+ super message
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Trailblazer
2
+ class Finder
3
+ module Error
4
+ class InvalidDefinedByValue < ArgumentError
5
+ def initialize(filter_by, defined_bys, value)
6
+ super "Invalid value '#{value}' used for defined_by #{filter_by} (expected one of #{defined_bys.join(', ')})"
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ module Trailblazer
2
+ class Finder
3
+ module Error
4
+ class InvalidNumber < ArgumentError
5
+ attr_reader :field, :number
6
+
7
+ def initialize(field, number)
8
+ @field = field
9
+ @number = number
10
+
11
+ super "#{field} should be more than 0. Currently '#{number}' is given."
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ module Trailblazer
2
+ class Finder
3
+ module Error
4
+ class MissingEntityType < ArgumentError
5
+ def initialize(message = 'No Entity Type provided. Entity Type can be defined on a class level or passed as an option.')
6
+ super message
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Trailblazer
2
+ class Finder
3
+ module Error
4
+ class WithIgnored < ArgumentError
5
+ def initialize(message = "defined_by filter_by's don't accept :with")
6
+ super message
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,55 @@
1
+ module Trailblazer
2
+ class Finder
3
+ module Features
4
+ module Paging
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ attr_reader :page, :per_page
10
+
11
+ def initialize(options = {})
12
+ @page = [options[:page].to_i, 0].max
13
+ @per_page = self.class.calculate_per_page options[:per_page]
14
+
15
+ super options
16
+ end
17
+
18
+ private
19
+
20
+ def fetch_results
21
+ apply_paging super
22
+ end
23
+
24
+ # Adapters will overwite this method
25
+ def apply_paging(entity_type)
26
+ entity_type.drop(([page, 1].max - 1) * per_page).first(per_page)
27
+ end
28
+
29
+ module ClassMethods
30
+ def per_page(number)
31
+ raise Error::InvalidNumber.new('Per page', number) unless number > 0
32
+ config[:per_page] = number
33
+ end
34
+
35
+ def min_per_page(number)
36
+ raise Error::InvalidNumber.new('Min per page', number) unless number > 0
37
+ config[:min_per_page] = number
38
+ end
39
+
40
+ def max_per_page(number)
41
+ raise Error::InvalidNumber.new('Max per page', number) unless number > 0
42
+ config[:max_per_page] = number
43
+ end
44
+
45
+ def calculate_per_page(given)
46
+ per_page = (given || config[:per_page] || 25).to_i.abs
47
+ per_page = [per_page, config[:max_per_page]].min if config[:max_per_page]
48
+ per_page = [per_page, config[:min_per_page]].max if config[:min_per_page]
49
+ per_page
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,66 @@
1
+ module Trailblazer
2
+ class Finder
3
+ module Features
4
+ module Sorting
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ base.instance_eval do
8
+ filter_by :sort do |entity_type, _|
9
+ sort_it(entity_type, sort_attribute, sort_direction)
10
+ end
11
+ end
12
+ end
13
+
14
+ # Adapters will overwite this method
15
+ def sort_it(entity_type, sort_attribute, sort_direction)
16
+ case sort_direction
17
+ when 'asc', 'ascending'
18
+ entity_type.sort_by { |a| a[sort_attribute.to_sym] }
19
+ when 'desc', 'descending'
20
+ entity_type.sort_by { |a| a[sort_attribute.to_sym] }.reverse
21
+ end
22
+ end
23
+
24
+ def sort?(attribute)
25
+ attribute == sort || sort.to_s.start_with?("#{attribute} ")
26
+ end
27
+
28
+ def sort_attribute
29
+ @sort_attribute ||= Utils::Extra.ensure_included sort.to_s.split(' ', 2).first, self.class.sort_attributes
30
+ end
31
+
32
+ def sort_direction
33
+ @sort_direction ||= Utils::Extra.ensure_included sort.to_s.split(' ', 2).last, %w[desc asc]
34
+ end
35
+
36
+ def sort_direction_for(attribute)
37
+ if sort_attribute == attribute.to_s
38
+ reverted_sort_direction
39
+ else
40
+ 'desc'
41
+ end
42
+ end
43
+
44
+ def sort_params_for(attribute, options = {})
45
+ options['sort'] = "#{attribute} #{sort_direction_for(attribute)}"
46
+ params options
47
+ end
48
+
49
+ def reverted_sort_direction
50
+ sort_direction == 'desc' ? 'asc' : 'desc'
51
+ end
52
+
53
+ module ClassMethods
54
+ def sortable_by(*attributes)
55
+ config[:sort_attributes] = attributes.map(&:to_s)
56
+ config[:defaults]['sort'] = "#{config[:sort_attributes].first} desc"
57
+ end
58
+
59
+ def sort_attributes
60
+ config[:sort_attributes] ||= []
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,22 @@
1
+ require 'trailblazer/finder/features/paging'
2
+ require 'trailblazer/finder/features/sorting'
3
+
4
+ module Trailblazer
5
+ class Finder
6
+ # Features
7
+ module Features
8
+ def self.included(base)
9
+ base.extend ClassMethods
10
+ end
11
+
12
+ # ClassMethods
13
+ module ClassMethods
14
+ def features(*mods)
15
+ mods.each do |mod|
16
+ include mod
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end