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
@@ -0,0 +1,66 @@
1
+ module Trailblazer
2
+ class Finder
3
+ module Filter
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ def filter_by(name, options = nil, &block)
10
+ return super unless options.is_a?(Hash) && options[:defined_by]
11
+
12
+ raise Error::BlockIgnored if block
13
+ raise Error::WithIgnored if options[:with]
14
+
15
+ handler = Handler.build(name, options[:defined_by])
16
+
17
+ super(name, options, &handler)
18
+ end
19
+ end
20
+
21
+ module Handler
22
+ module_function
23
+
24
+ def build(name, defined_bys) # rubocop:disable Metrics/MethodLength
25
+ defined_bys = defined_bys.map(&:to_s)
26
+ handler = self
27
+ lambda do |entity_type, value|
28
+ handler.apply_filter(
29
+ object: self,
30
+ filter_by: name,
31
+ defined_bys: defined_bys,
32
+ entity_type: entity_type,
33
+ value: value
34
+ )
35
+ end
36
+ end
37
+
38
+ def apply_filter(object:, filter_by:, defined_bys:, entity_type:, value:) # rubocop:disable Metrics/MethodLength
39
+ return if value.nil? || value == ''
40
+
41
+ unless defined_bys.include? value
42
+ return handle_invalid_value(
43
+ object: object,
44
+ filter_by: filter_by,
45
+ defined_bys: defined_bys,
46
+ entity_type: entity_type,
47
+ value: value
48
+ )
49
+ end
50
+
51
+ object.send("apply_#{filter_by}_with_#{Utils::String.underscore(value)}", entity_type)
52
+ end
53
+
54
+ def handle_invalid_value(object:, filter_by:, defined_bys:, entity_type:, value:)
55
+ specific = "handle_invalid_#{filter_by}"
56
+ return object.send(specific, entity_type, value) if object.respond_to? specific, true
57
+
58
+ catch_all = 'handle_invalid_defined_by'
59
+ return object.send(catch_all, filter_by, entity_type, value) if object.respond_to? catch_all, true
60
+
61
+ raise Error::InvalidDefinedByValue.new(filter_by, defined_bys, value)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,29 @@
1
+ module Trailblazer
2
+ class Finder
3
+ class Find
4
+ attr_reader :params
5
+
6
+ def initialize(entity_type, params, actions)
7
+ @entity_type = entity_type
8
+ @actions = actions
9
+ @params = params
10
+ end
11
+
12
+ def param(name)
13
+ @params[name]
14
+ end
15
+
16
+ def query(context)
17
+ @params.inject(@entity_type) do |entity_type, (name, value)|
18
+ value = Utils::Parse.date(value) if Utils::Parse.date(value)
19
+ new_entity_type = context.instance_exec entity_type, value, &@actions[name]
20
+ new_entity_type || entity_type
21
+ end
22
+ end
23
+
24
+ def count(context)
25
+ query(context).count
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,31 @@
1
+ module Trailblazer
2
+ class Finder
3
+ # Helper module
4
+ module Utils
5
+ class Extra
6
+ def self.ensure_included(item, collection)
7
+ if collection.include? item
8
+ item
9
+ else
10
+ collection.first
11
+ end
12
+ end
13
+
14
+ def self.deep_copy(object) # rubocop:disable Metrics/MethodLength
15
+ case object
16
+ when Array
17
+ object.map { |element| deep_copy(element) }
18
+ when Hash
19
+ object.each_with_object({}) do |(key, value), result|
20
+ result[key] = deep_copy(value)
21
+ end
22
+ when NilClass, FalseClass, TrueClass, Symbol, Method, Numeric
23
+ object
24
+ else
25
+ object.dup
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,28 @@
1
+ # TODO: Clean this shit up
2
+ module Trailblazer
3
+ class Finder
4
+ # Helper module
5
+ module Utils
6
+ module Params
7
+ module_function
8
+
9
+ def stringify_keys(hash)
10
+ hash = hash.to_unsafe_h if hash.respond_to? :to_unsafe_h
11
+ Hash[(hash || {}).map { |k, v| [k.to_s, v] }]
12
+ end
13
+
14
+ def slice_keys(hash, keys)
15
+ keys.each_with_object({}) do |key, memo|
16
+ memo[key] = hash[key] if hash.key? key
17
+ end
18
+ end
19
+
20
+ def normalize_params(defaults, filters, keys)
21
+ (defaults || {}).merge(
22
+ slice_keys(stringify_keys(filters || {}), keys || [])
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,25 @@
1
+ require 'date'
2
+
3
+ module Trailblazer
4
+ class Finder
5
+ # Helper module
6
+ module Utils
7
+ class Parse
8
+ # Need a replacement for this
9
+ def self.date(value)
10
+ return if value.nil?
11
+ return if [true, false].include? value
12
+ return if value.is_a? Integer
13
+ return if value =~ /[[:alpha:]]/
14
+ Date.parse(value).strftime('%Y-%m-%d')
15
+ rescue ArgumentError
16
+ nil
17
+ end
18
+
19
+ def self.term(value)
20
+ "%#{value.gsub(/\s+/, '%')}%"
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,35 @@
1
+ require 'date'
2
+
3
+ module Trailblazer
4
+ class Finder
5
+ # Helper module
6
+ module Utils
7
+ class String
8
+ def self.blank?(value)
9
+ return false if num?(value)
10
+ value.nil? || value.strip.empty?
11
+ end
12
+
13
+ def self.num?(str)
14
+ Integer(str)
15
+ rescue ArgumentError, TypeError
16
+ false
17
+ end
18
+
19
+ def self.camelize(text)
20
+ text.to_s.gsub(/(?:^|_)(.)/) { Regexp.last_match[1].upcase }
21
+ end
22
+
23
+ def self.underscore(text)
24
+ text
25
+ .to_s.gsub(/::/, '/')
26
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
27
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
28
+ .tr('-', '_')
29
+ .tr(' ', '_')
30
+ .downcase
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ module Trailblazer
2
+ class Finder
3
+ VERSION = '0.1.0'.freeze
4
+ end
5
+ end
@@ -0,0 +1,29 @@
1
+ require 'trailblazer/finder/version'
2
+ require 'trailblazer/finder/errors/block_ignored'
3
+ require 'trailblazer/finder/errors/invalid_defined_by_value'
4
+ require 'trailblazer/finder/errors/invalid_number'
5
+ require 'trailblazer/finder/errors/missing_entity_type'
6
+ require 'trailblazer/finder/errors/with_ignored'
7
+ require 'trailblazer/finder'
8
+ require 'trailblazer/finder/utils/params'
9
+ require 'trailblazer/finder/utils/parse'
10
+ require 'trailblazer/finder/utils/string'
11
+ require 'trailblazer/finder/utils/extra'
12
+ require 'trailblazer/finder/base'
13
+ require 'trailblazer/finder/find'
14
+ require 'trailblazer/finder/filter'
15
+ require 'trailblazer/finder/features'
16
+ require 'trailblazer/finder/adapters'
17
+
18
+ # :nocov:
19
+ require 'trailblazer/operation/finder' if Gem.loaded_specs.key?('trailblazer')
20
+ # :nocov:
21
+
22
+ module Trailblazer
23
+ class Finder
24
+ include Base
25
+ include Filter
26
+ include Features
27
+ include Adapters
28
+ end
29
+ end
@@ -0,0 +1,61 @@
1
+
2
+ # Really gotta clean this up, but can't be bothered right now
3
+ module Trailblazer
4
+ class Operation
5
+ def self.Finder(finder_class, action = nil, entity_type = nil)
6
+ task = Trailblazer::Activity::TaskBuilder::Binary.call(Finder.new)
7
+
8
+ extension = Trailblazer::Activity::TaskWrap::Merge.new(
9
+ Wrap::Inject::Defaults(
10
+ 'finder.class' => finder_class,
11
+ 'finder.entity_type' => entity_type,
12
+ 'finder.action' => action
13
+ )
14
+ )
15
+
16
+ { task: task, id: 'finder.build', extension: [extension] }
17
+ end
18
+
19
+ class Finder
20
+ def call(options, params:, **)
21
+ builder = Finder::Builder.new
22
+ options[:finder] = finder = builder.call(options, params)
23
+ options['result.finder'] = result = Result.new(!finder.nil?, {})
24
+
25
+ result.success?
26
+ end
27
+
28
+ class Builder
29
+ def call(options, params)
30
+ finder_class = options['finder.class']
31
+ entity_type = options['finder.entity_type'] || nil
32
+ action = options['finder.action'] || :all
33
+ action = :all unless %i[all single].include?(action)
34
+
35
+ send("#{action}!", finder_class, entity_type, params, options['finder.action'])
36
+ end
37
+
38
+ private
39
+
40
+ def all!(finder_class, entity_type, params, *)
41
+ finder_class.new(entity_type: entity_type, filter: params[:f], page: params[:page], per_page: params[:per_page])
42
+ end
43
+
44
+ def single!(finder_class, entity_type, params, *)
45
+ apply_id(params)
46
+ if entity_type.nil?
47
+ finder_class.new(filter: params[:f], page: params[:page], per_page: params[:per_page]).results.first
48
+ else
49
+ finder_class.new(entity_type: entity_type, filter: params[:f]).results.first
50
+ end
51
+ end
52
+
53
+ def apply_id(params)
54
+ return if params[:id].nil?
55
+ params[:f] = {} unless params.key?('f')
56
+ params[:f][:id] = params[:id] unless params[:f].key?('id')
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,15 @@
1
+ require 'bundler/setup'
2
+ require 'simplecov'
3
+ SimpleCov.start do
4
+ add_group 'Trailblazer-Finder', 'lib'
5
+ add_group 'Tests', 'spec'
6
+ end
7
+
8
+ RSpec.configure do |config|
9
+ config.expect_with(:rspec) { |c| c.syntax = :expect }
10
+ end
11
+
12
+ require 'coveralls'
13
+ Coveralls.wear!
14
+
15
+ require 'trailblazer/finder'
@@ -0,0 +1,50 @@
1
+ require_relative 'spec_helper'
2
+ require_relative 'support/paging_shared_example'
3
+ require_relative 'support/sorting_shared_example'
4
+ require 'active_record'
5
+
6
+ ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
7
+
8
+ ActiveRecord::Schema.define do
9
+ self.verbose = false
10
+
11
+ create_table :products, force: true do |t|
12
+ t.string :name
13
+ t.string :slug
14
+ t.integer :category_id
15
+ t.integer :price
16
+
17
+ t.timestamps null: true
18
+ end
19
+
20
+ create_table :categories, force: true do |t|
21
+ t.string :title
22
+ t.string :slug
23
+
24
+ t.timestamps null: true
25
+ end
26
+ end
27
+
28
+ class Product < ActiveRecord::Base
29
+ belongs_to :category
30
+ end
31
+
32
+ class Category < ActiveRecord::Base
33
+ end
34
+
35
+ module ActiveRecord
36
+ class Base
37
+ def self.reset_pk_sequence
38
+ case ActiveRecord::Base.connection.adapter_name
39
+ when 'SQLite'
40
+ new_max = maximum(primary_key) || 0
41
+ update_seq_sql = "update sqlite_sequence set seq = #{new_max} where name = '#{table_name}';"
42
+ ActiveRecord::Base.connection.execute(update_seq_sql)
43
+ when 'PostgreSQL'
44
+ ActiveRecord::Base.connection.reset_pk_sequence!(table_name)
45
+ else
46
+ raise 'Task not implemented for this DB adapter'
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,35 @@
1
+ require_relative 'spec_helper'
2
+ require_relative 'support/paging_shared_example'
3
+ require 'data_mapper'
4
+
5
+ DataMapper.setup(:default, 'sqlite::memory:')
6
+
7
+ class DProduct
8
+ include DataMapper::Resource
9
+
10
+ property :id, Serial
11
+ property :name, String
12
+ property :slug, String
13
+ property :d_category_id, Integer
14
+ property :price, Integer
15
+ property :created_at, DateTime
16
+ property :updated_at, DateTime
17
+
18
+ belongs_to :d_category
19
+ end
20
+
21
+ class DCategory
22
+ include DataMapper::Resource
23
+
24
+ property :id, Serial
25
+ property :title, String
26
+ property :slug, String
27
+ property :created_at, DateTime
28
+ property :updated_at, DateTime
29
+
30
+ has n, :d_product
31
+ end
32
+
33
+ DataMapper.finalize
34
+ require 'dm-migrations'
35
+ DataMapper.auto_migrate!
@@ -0,0 +1,32 @@
1
+ require_relative 'spec_helper'
2
+ require_relative 'support/paging_shared_example'
3
+ require_relative 'support/sorting_shared_example'
4
+ require 'sequel'
5
+ Sequel::Model.plugin :timestamps
6
+ DB = Sequel.sqlite
7
+
8
+ DB.create_table :s_products do
9
+ primary_key :id
10
+ String :name
11
+ String :slug
12
+ Integer :s_category_id
13
+ Integer :price
14
+ DateTime :created_at
15
+ DateTime :updated_at
16
+ end
17
+
18
+ DB.create_table :s_categories do
19
+ primary_key :id
20
+ String :title
21
+ String :slug
22
+ DateTime :created_at
23
+ DateTime :updated_at
24
+ end
25
+
26
+ class SProduct < Sequel::Model
27
+ many_to_one :s_category
28
+ end
29
+
30
+ class SCategory < Sequel::Model
31
+ one_to_many :s_product
32
+ end
@@ -0,0 +1,65 @@
1
+ shared_examples_for 'a paging feature' do
2
+ describe '#page' do
3
+ it 'treats nil page as 0' do
4
+ finder = finder_with_page nil
5
+ expect(finder.page).to eq 0
6
+ end
7
+
8
+ it 'treats negative page numbers as positive' do
9
+ finder = finder_with_page(-1)
10
+ expect(finder.page).to eq 0
11
+ end
12
+ end
13
+
14
+ describe '#per_page' do
15
+ it 'returns the class defined per page' do
16
+ finder = finder_class.new
17
+ expect(finder.per_page).to eq 2
18
+ end
19
+
20
+ it 'can be overwritten as option' do
21
+ finder = finder_class.new per_page: 3
22
+ expect(finder.per_page).to eq 3
23
+ end
24
+
25
+ it 'respects min per page' do
26
+ finder = finder_class.new per_page: 1
27
+ expect(finder.per_page).to eq 2
28
+ end
29
+
30
+ it 'respects max per page' do
31
+ finder = finder_class.new per_page: 100
32
+ expect(finder.per_page).to eq 10
33
+ end
34
+ end
35
+
36
+ describe '.per_page' do
37
+ it 'does not accept 0' do
38
+ expect { define_finder_class { per_page(0) } }.to raise_error Trailblazer::Finder::Error::InvalidNumber
39
+ end
40
+
41
+ it 'does not accept negative number' do
42
+ expect { define_finder_class { per_page(-1) } }.to raise_error Trailblazer::Finder::Error::InvalidNumber
43
+ end
44
+ end
45
+
46
+ describe '.min_per_page' do
47
+ it 'does not accept 0' do
48
+ expect { define_finder_class { min_per_page(0) } }.to raise_error Trailblazer::Finder::Error::InvalidNumber
49
+ end
50
+
51
+ it 'does not accept negative number' do
52
+ expect { define_finder_class { min_per_page(-1) } }.to raise_error Trailblazer::Finder::Error::InvalidNumber
53
+ end
54
+ end
55
+
56
+ describe '.max_per_page' do
57
+ it 'does not accept 0' do
58
+ expect { define_finder_class { max_per_page(0) } }.to raise_error Trailblazer::Finder::Error::InvalidNumber
59
+ end
60
+
61
+ it 'does not accept negative number' do
62
+ expect { define_finder_class { max_per_page(-1) } }.to raise_error Trailblazer::Finder::Error::InvalidNumber
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,95 @@
1
+ shared_examples_for 'a sorting feature' do
2
+ describe '#sort?' do
3
+ it 'matches the sort option' do
4
+ finder = finder_with_sort 'price desc'
5
+
6
+ expect(finder).to be_sort :price
7
+ expect(finder).not_to be_sort :name
8
+ end
9
+
10
+ it 'matches string also' do
11
+ finder = finder_with_sort 'price desc'
12
+
13
+ expect(finder).to be_sort 'price'
14
+ expect(finder).not_to be_sort 'name'
15
+ end
16
+
17
+ it 'matches exact strings' do
18
+ finder = finder_with_sort 'price desc'
19
+
20
+ expect(finder).to be_sort 'price desc'
21
+ expect(finder).not_to be_sort 'price asc'
22
+ end
23
+ end
24
+
25
+ describe '#sort_attribute' do
26
+ it 'returns sort option attribute' do
27
+ finder = finder_with_sort 'price desc'
28
+ expect(finder.sort_attribute).to eq 'price'
29
+ end
30
+
31
+ it 'defaults to the first sort by option' do
32
+ finder = finder_with_sort
33
+ expect(finder.sort_attribute).to eq 'name'
34
+ end
35
+
36
+ it 'rejects invalid sort options, uses defaults' do
37
+ finder = finder_with_sort 'invalid'
38
+ expect(finder.sort_attribute).to eq 'name'
39
+ end
40
+ end
41
+
42
+ describe '#sort_direction' do
43
+ it 'returns asc or desc' do
44
+ expect(finder_with_sort('price desc').sort_direction).to eq 'desc'
45
+ expect(finder_with_sort('price asc').sort_direction).to eq 'asc'
46
+ end
47
+
48
+ it 'defaults to desc' do
49
+ expect(finder_with_sort.sort_direction).to eq 'desc'
50
+ expect(finder_with_sort('price').sort_direction).to eq 'desc'
51
+ end
52
+
53
+ it 'rejects invalid sort options, uses desc' do
54
+ expect(finder_with_sort('price foo').sort_direction).to eq 'desc'
55
+ end
56
+ end
57
+
58
+ describe '#sort_direction_for' do
59
+ it 'returns desc if current sort attribute is not the given attribute' do
60
+ expect(finder_with_sort('price desc').sort_direction_for('name')).to eq 'desc'
61
+ end
62
+
63
+ it 'returns asc if current sort attribute is the given attribute' do
64
+ expect(finder_with_sort('name desc').sort_direction_for('name')).to eq 'asc'
65
+ end
66
+
67
+ it 'returns desc if current sort attribute is the given attribute, but asc with direction' do
68
+ expect(finder_with_sort('name asc').sort_direction_for('name')).to eq 'desc'
69
+ end
70
+ end
71
+
72
+ describe '#sort_params_for' do
73
+ it 'adds sort direction' do
74
+ finder = finder_with_sort 'name', name: 'test'
75
+ expect(finder.sort_params_for(:price)).to eq 'sort' => 'price desc', 'name' => 'test'
76
+ end
77
+
78
+ it 'reverses sort direction if this is the current sort attribute' do
79
+ finder = finder_with_sort 'name desc', name: 'test'
80
+ expect(finder.sort_params_for(:name)).to eq 'sort' => 'name asc', 'name' => 'test'
81
+ end
82
+
83
+ it 'accepts additional options' do
84
+ finder = finder_with_sort
85
+ expect(finder.sort_params_for(:price, name: 'value')).to eq 'sort' => 'price desc', 'name' => 'value'
86
+ end
87
+ end
88
+
89
+ describe '#reverted_sort_direction' do
90
+ it 'reverts sorting direction' do
91
+ expect(finder_with_sort('price desc').reverted_sort_direction).to eq 'asc'
92
+ expect(finder_with_sort('price asc').reverted_sort_direction).to eq 'desc'
93
+ end
94
+ end
95
+ end