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