trailblazer-finder 0.1.0

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