sortable-by 0.10.0 → 0.13.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5b5117bc573fa2cf18ccee9dec86c53cd09920af83463e161ad97c34a50c87c4
4
- data.tar.gz: 47bed91f217dfc04a5d9180040a18a163e0f2da6597059bb73ce8390c01d5d6e
3
+ metadata.gz: 4639f942b5f628b07367a343160d7e4301b00bf3c6e0db9f57ff43d204a85ab5
4
+ data.tar.gz: 2f424ec2cda37112ca64fdf628b7ff0aee3e223dbee81d585040eff79ee468c8
5
5
  SHA512:
6
- metadata.gz: 68712cabc1e8d2bc54d4716d27cd896170fad207448814a0c784f7a018bd2da8b0fad7d26ba1cfca8cb884969665f014c3aa0912c457fbb2465a1fa4c7214c5c
7
- data.tar.gz: 9d9b0f42b50c94cbcd36eec6cb2b1c1367150943a6c252092d84d45b3aecdb5ea1431ff227faa68db045b32fc02983f4d0bcb41fa1619a9f28d4a1ffd1789666
6
+ metadata.gz: ff08a62e083e324f9baa4118423c82d90ad93d00f42b4cd6ae74d915a637439315f4cf5ebd8b9d598075bd9565064429a06d0017bae3b5f7998c35b39a50f14c
7
+ data.tar.gz: b084242394054b896d594c923e262c6325a74c58c836e2224ee5c4f91c3bc087bedfe0b42eb7aa2438ea2a84dd41ddd54c6b5718f68e181d5869bbb5efcb2397
data/.gitignore CHANGED
@@ -1,3 +1,5 @@
1
1
  spec/tmp
2
2
  pkg
3
3
  *.gem
4
+ .rake_tasks~
5
+ .rubocop-*
@@ -1,18 +1,12 @@
1
+ inherit_from:
2
+ - https://gitlab.com/bsm/misc/raw/master/rubocop/default.yml
3
+
1
4
  AllCops:
2
- TargetRubyVersion: 2.2
5
+ TargetRubyVersion: 2.5
3
6
 
4
- Metrics/AbcSize:
7
+ Naming/MethodParameterName:
8
+ MinNameLength: 2
9
+ Naming/MemoizedInstanceVariableName:
5
10
  Enabled: false
6
11
  Naming/FileName:
7
12
  Exclude: [lib/sortable-by.rb]
8
- Metrics/MethodLength:
9
- Max: 20
10
- Metrics/LineLength:
11
- Max: 120
12
-
13
- Style/TrailingCommaInArguments:
14
- EnforcedStyleForMultiline: consistent_comma
15
- Style/TrailingCommaInArrayLiteral:
16
- EnforcedStyleForMultiline: consistent_comma
17
- Style/TrailingCommaInHashLiteral:
18
- EnforcedStyleForMultiline: consistent_comma
@@ -1,8 +1,9 @@
1
1
  sudo: false
2
2
  rvm:
3
- - 2.2
4
- - 2.3
5
- - 2.4
6
3
  - 2.5
4
+ - 2.6
5
+ - 2.7
7
6
  gemfile:
8
7
  - Gemfile
8
+ before_install:
9
+ - gem install bundler
@@ -1,63 +1,68 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sortable-by (0.10.0)
4
+ sortable-by (0.13.0)
5
5
  activerecord
6
6
  activesupport
7
7
 
8
8
  GEM
9
9
  remote: http://rubygems.org/
10
10
  specs:
11
- activemodel (5.1.5)
12
- activesupport (= 5.1.5)
13
- activerecord (5.1.5)
14
- activemodel (= 5.1.5)
15
- activesupport (= 5.1.5)
16
- arel (~> 8.0)
17
- activesupport (5.1.5)
11
+ activemodel (6.0.3.3)
12
+ activesupport (= 6.0.3.3)
13
+ activerecord (6.0.3.3)
14
+ activemodel (= 6.0.3.3)
15
+ activesupport (= 6.0.3.3)
16
+ activesupport (6.0.3.3)
18
17
  concurrent-ruby (~> 1.0, >= 1.0.2)
19
- i18n (~> 0.7)
18
+ i18n (>= 0.7, < 2)
20
19
  minitest (~> 5.1)
21
20
  tzinfo (~> 1.1)
22
- arel (8.0.0)
23
- ast (2.4.0)
24
- concurrent-ruby (1.0.5)
25
- diff-lcs (1.3)
26
- i18n (0.9.5)
21
+ zeitwerk (~> 2.2, >= 2.2.2)
22
+ ast (2.4.1)
23
+ concurrent-ruby (1.1.7)
24
+ diff-lcs (1.4.4)
25
+ i18n (1.8.5)
27
26
  concurrent-ruby (~> 1.0)
28
- minitest (5.11.3)
29
- parallel (1.12.1)
30
- parser (2.5.0.4)
31
- ast (~> 2.4.0)
32
- powerpack (0.1.1)
27
+ minitest (5.14.2)
28
+ parallel (1.19.2)
29
+ parser (2.7.2.0)
30
+ ast (~> 2.4.1)
33
31
  rainbow (3.0.0)
34
- rake (12.3.0)
35
- rspec (3.7.0)
36
- rspec-core (~> 3.7.0)
37
- rspec-expectations (~> 3.7.0)
38
- rspec-mocks (~> 3.7.0)
39
- rspec-core (3.7.1)
40
- rspec-support (~> 3.7.0)
41
- rspec-expectations (3.7.0)
32
+ rake (13.0.1)
33
+ regexp_parser (1.8.1)
34
+ rexml (3.2.4)
35
+ rspec (3.9.0)
36
+ rspec-core (~> 3.9.0)
37
+ rspec-expectations (~> 3.9.0)
38
+ rspec-mocks (~> 3.9.0)
39
+ rspec-core (3.9.3)
40
+ rspec-support (~> 3.9.3)
41
+ rspec-expectations (3.9.2)
42
42
  diff-lcs (>= 1.2.0, < 2.0)
43
- rspec-support (~> 3.7.0)
44
- rspec-mocks (3.7.0)
43
+ rspec-support (~> 3.9.0)
44
+ rspec-mocks (3.9.1)
45
45
  diff-lcs (>= 1.2.0, < 2.0)
46
- rspec-support (~> 3.7.0)
47
- rspec-support (3.7.1)
48
- rubocop (0.53.0)
46
+ rspec-support (~> 3.9.0)
47
+ rspec-support (3.9.3)
48
+ rubocop (0.92.0)
49
49
  parallel (~> 1.10)
50
- parser (>= 2.5)
51
- powerpack (~> 0.1)
50
+ parser (>= 2.7.1.5)
52
51
  rainbow (>= 2.2.2, < 4.0)
52
+ regexp_parser (>= 1.7)
53
+ rexml
54
+ rubocop-ast (>= 0.5.0)
53
55
  ruby-progressbar (~> 1.7)
54
- unicode-display_width (~> 1.0, >= 1.0.1)
55
- ruby-progressbar (1.9.0)
56
- sqlite3 (1.3.13)
56
+ unicode-display_width (>= 1.4.0, < 2.0)
57
+ rubocop-ast (0.7.1)
58
+ parser (>= 2.7.1.5)
59
+ ruby-progressbar (1.10.1)
60
+ sqlite3 (1.4.2)
57
61
  thread_safe (0.3.6)
58
- tzinfo (1.2.5)
62
+ tzinfo (1.2.7)
59
63
  thread_safe (~> 0.1)
60
- unicode-display_width (1.3.0)
64
+ unicode-display_width (1.7.0)
65
+ zeitwerk (2.4.0)
61
66
 
62
67
  PLATFORMS
63
68
  ruby
@@ -71,4 +76,4 @@ DEPENDENCIES
71
76
  sqlite3
72
77
 
73
78
  BUNDLED WITH
74
- 1.16.1
79
+ 2.1.4
data/README.md CHANGED
@@ -13,38 +13,75 @@ Add `gem 'sortable-by'` to your Gemfile.
13
13
 
14
14
  ## Usage
15
15
 
16
- Simple use case:
16
+ Simple:
17
17
 
18
18
  ```ruby
19
- class Foo < ActiveRecord::Base
20
- sortable_by :title, :updated_at, default: { updated_at: :desc }
19
+ class Post < ActiveRecord::Base
20
+ sortable_by :title, :id
21
21
  end
22
22
 
23
- Foo.sorted_by "-updated_at,title" # => ORDER BY foos.updated_at DESC, foos.title ASC
24
- Foo.sorted_by "bad,title" # => ORDER BY foos.title ASC
25
- Foo.sorted_by nil # => ORDER BY foos.updated_at DESC
23
+ Post.sorted_by('title') # => ORDER BY LOWER(posts.title) ASC
24
+ Post.sorted_by('-title') # => ORDER BY LOWER(posts.title) DESC
25
+ Post.sorted_by('bad,title') # => ORDER BY LOWER(posts.title) ASC
26
+ Post.sorted_by(nil) # => ORDER BY LOWER(posts.title) ASC
26
27
  ```
27
28
 
28
- Aliases and composition:
29
+ Case-sensitive:
29
30
 
30
31
  ```ruby
31
- class Foo < ActiveRecord::Base
32
- sortable_by semver: %i[major minor patch], default: { id: :asc }
32
+ class Post < ActiveRecord::Base
33
+ sortable_by do |x|
34
+ x.field :title, case_sensitive: true
35
+ x.field :id
36
+ end
33
37
  end
34
38
 
35
- Foo.sorted_by "semver" # => ORDER BY foos.major ASC, foos.minor ASC, foos.patch ASC
36
- Foo.sorted_by "-semver" # => ORDER BY foos.major DESC, foos.minor DESC, foos.patch DESC
37
- Foo.sorted_by nil # => ORDER BY foos.id ASC
39
+ Post.sorted_by('title') # => ORDER BY posts.title ASC
38
40
  ```
39
41
 
40
- Custom functions:
42
+ With custom default:
41
43
 
42
44
  ```ruby
43
- class Foo < ActiveRecord::Base
44
- sortable_by insensitive: Arel::Nodes::NamedFunction.new('LOWER', [arel_table[:title]]), default: { id: :asc }
45
+ class Post < ActiveRecord::Base
46
+ sortable_by :id, :topic, :created_at, default: 'topic,-created_at'
45
47
  end
46
48
 
47
- Foo.sorted_by "insensitive" # => ORDER BY LOWER(foos.title) ASC
48
- Foo.sorted_by "-insensitive" # => ORDER BY LOWER(foos.title) DESC
49
- Foo.sorted_by nil # => ORDER BY foos.id ASC
49
+ Post.sorted_by(nil) # => ORDER BY LOWER(posts.topic) ASC, posts.created_at DESC
50
+ ```
51
+
52
+ Composition:
53
+
54
+ ```ruby
55
+ class App < ActiveRecord::Base
56
+ sortable_by :name, default: '-version' do |x|
57
+ x.field :version, as: %i[major minor patch]]
58
+ end
59
+ end
60
+
61
+ App.sorted_by('version') # => ORDER BY apps.major ASC, apps.minor ASC, apps.patch ASC
62
+ App.sorted_by(nil) # => ORDER BY apps.major DESC, apps.minor DESC, apps.patch DESC
63
+ ```
64
+
65
+ Associations (eager load):
66
+
67
+ ```ruby
68
+ class Product < ActiveRecord::Base
69
+ belongs_to :shop
70
+ sortable_by do |x|
71
+ x.field :name
72
+ x.field :shop, as: Shop.arel_table[:name], eager_load: :shop
73
+ x.default 'shop,name'
74
+ end
75
+ end
76
+ ```
77
+
78
+ Associations (custom scope):
79
+
80
+ ```ruby
81
+ class Product < ActiveRecord::Base
82
+ belongs_to :shop
83
+ sortable_by do |x|
84
+ x.field :shop, as: Shop.arel_table[:name], scope: -> { joins(:shop) }
85
+ end
86
+ end
50
87
  ```
data/Rakefile CHANGED
@@ -6,4 +6,4 @@ require 'rubocop/rake_task'
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
  RuboCop::RakeTask.new(:rubocop)
8
8
 
9
- task default: :spec
9
+ task default: %i[spec rubocop]
@@ -1,71 +1,44 @@
1
1
  require 'active_record'
2
- require 'active_support/concern'
3
- require 'set'
4
2
 
5
3
  module ActiveRecord # :nodoc:
6
- module SortableByHelper # :nodoc:
7
- extend ActiveSupport::Concern
4
+ module SortableBy # :nodoc:
5
+ class Config # :nodoc:
6
+ attr_reader :_fields, :_default
8
7
 
9
- included do
10
- class_attribute :_sortable_by_scope_options, instance_accessor: false
11
- sortable_by
12
- end
13
-
14
- def self.validate_custom_scope!(custom, original = custom)
15
- case custom
16
- when true, Arel::Nodes::Node, String, Symbol
17
- nil # OK
18
- when Array
19
- custom.each do |v|
20
- validate_custom_scope!(v, original)
21
- end
22
- else
23
- raise ArgumentError, "Option #{original.inspect} contains unexpected values."
8
+ def initialize
9
+ @_fields = {}
10
+ @_default = nil
24
11
  end
25
- end
26
12
 
27
- def self.order_clause(name, rank, value)
28
- case value
29
- when true
30
- { name => rank }
31
- when String, Symbol
32
- { value => rank }
33
- when Arel::Nodes::Node
34
- value.send(rank)
35
- when Array
36
- value.map { |v| order_clause(name, rank, v) }
13
+ def dup
14
+ duplicate = self.class.new
15
+ duplicate.instance_variable_set :@_fields, _fields.deep_dup
16
+ duplicate.instance_variable_set :@_default, _default.deep_dup
17
+ duplicate
37
18
  end
38
- end
39
19
 
40
- module ClassMethods # :nodoc:
41
- # Copy _sortable_by_scope_options on inheritance.
42
- def inherited(base) #:nodoc:
43
- base._sortable_by_scope_options = _sortable_by_scope_options.deep_dup
44
- super
20
+ def field(name, **opts)
21
+ name = name.to_s
22
+ @_fields[name] = Field.new(name, **opts)
23
+ @_default ||= name
45
24
  end
46
25
 
47
- # Provide a whitelist and options for sorted_by
48
- def sortable_by(*whitelist)
49
- self._sortable_by_scope_options ||= { scopes: {}, default: { id: :asc } }
50
-
51
- opts = whitelist.extract_options!
52
- default = opts.delete(:default)
53
- self._sortable_by_scope_options[:default] = default if default
54
-
55
- whitelist.each do |attr|
56
- self._sortable_by_scope_options[:scopes][attr.to_s] = true
57
- end
58
- opts.each do |attr, custom|
59
- SortableByHelper.validate_custom_scope!(custom)
60
- self._sortable_by_scope_options[:scopes][attr.to_s] = custom
26
+ def default(expr)
27
+ # legacy support
28
+ if expr.is_a?(Hash)
29
+ expr = expr.map do |field, dir|
30
+ [(dir == :desc ? '-' : ''), field].join
31
+ end.join(',')
61
32
  end
33
+
34
+ @_default = expr.to_s
62
35
  end
63
36
 
64
- # @param [String] expr the sort expr
65
- # @return [ActiveRecord::Relation] the scoped relation
66
- def sorted_by(expr)
67
- relation = self
68
- matches = expr.to_s.split(',').count do |name|
37
+ protected
38
+
39
+ def order(relation, expr, fallback: true)
40
+ matched = false
41
+ expr.to_s.split(',').each do |name|
69
42
  name.strip!
70
43
 
71
44
  rank = :asc
@@ -74,23 +47,143 @@ module ActiveRecord # :nodoc:
74
47
  name = name[1..-1]
75
48
  end
76
49
 
77
- value = self._sortable_by_scope_options[:scopes][name]
78
- clause = SortableByHelper.order_clause(name, rank, value)
79
- relation = relation.order(clause) if clause
80
- clause
50
+ field = _fields[name]
51
+ next unless field
52
+
53
+ matched = true
54
+ relation = field.order(relation, rank)
81
55
  end
82
56
 
83
- if matches.zero?
84
- default = self._sortable_by_scope_options[:default]
85
- relation = relation.order(default)
57
+ relation = order(relation, _default, fallback: false) if fallback && !matched && _default
58
+ relation
59
+ end
60
+ end
61
+
62
+ class Field # :nodoc:
63
+ STRING_TYPES = %i[string text].freeze # :nodoc:
64
+
65
+ def initialize(name, as: nil, scope: nil, eager_load: nil, case_sensitive: false)
66
+ @cols = Array.wrap(as)
67
+ @eager_load = Array.wrap(eager_load).presence
68
+ @case_sensitive = case_sensitive == true
69
+
70
+ # validate custom_scope
71
+ @custom_scope = scope
72
+ raise ArgumentError, "Invalid sortable-by field '#{name}': scope must be a Proc." if @custom_scope && !@custom_scope.is_a?(Proc)
73
+
74
+ # normalize cols
75
+ @cols.push name if @cols.empty?
76
+ @cols.each do |col|
77
+ case col
78
+ when String, Symbol, Arel::Attributes::Attribute, Arel::Nodes::Node
79
+ next
80
+ when Proc
81
+ raise ArgumentError, "Invalid sortable-by field '#{name}': proc must accept 2 arguments." unless col.arity == 2
82
+ else
83
+ raise ArgumentError, "Invalid sortable-by field '#{name}': invalid type #{col.class}."
84
+ end
85
+ end
86
+ end
87
+
88
+ def order(relation, rank)
89
+ @cols.each do |col|
90
+ case col
91
+ when String, Symbol
92
+ type = relation.columns_hash[col.to_s].type
93
+ col = relation.arel_table[col]
94
+ col = col.lower if STRING_TYPES.include?(type) && !@case_sensitive
95
+ relation = relation.order(col.send(rank))
96
+ when Arel::Nodes::Node, Arel::Attributes::Attribute
97
+ relation = relation.order(col.send(rank))
98
+ when Proc
99
+ relation = col.call(relation, rank)
100
+ end
86
101
  end
87
102
 
103
+ relation = relation.eager_load(*@eager_load) if @eager_load
104
+ relation = relation.instance_eval(&@custom_scope) if @custom_scope
88
105
  relation
89
106
  end
90
107
  end
108
+
109
+ def self.extended(base) # :nodoc:
110
+ base.class_attribute :_sortable_by_config, instance_accessor: false, instance_predicate: false
111
+ base._sortable_by_config = Config.new
112
+ end
113
+
114
+ def inherited(base) # :nodoc:
115
+ base._sortable_by_config = _sortable_by_config.deep_dup
116
+ super
117
+ end
118
+
119
+ # Declare sortable attributes and scopes. Examples:
120
+ #
121
+ # # Simple
122
+ # class Post < ActiveRecord::Base
123
+ # sortable_by :title, :id
124
+ # end
125
+ #
126
+ # # Case-sensitive
127
+ # class Post < ActiveRecord::Base
128
+ # sortable_by do |x|
129
+ # x.field :title, case_sensitive: true
130
+ # x.field :id
131
+ # end
132
+ # end
133
+ #
134
+ # # With default
135
+ # class Post < ActiveRecord::Base
136
+ # sortable_by :id, :topic, :created_at,
137
+ # default: 'topic,-created_at'
138
+ # end
139
+ #
140
+ # # Composition
141
+ # class App < ActiveRecord::Base
142
+ # sortable_by :name, default: '-version' do |x|
143
+ # x.field :version, as: %i[major minor patch]]
144
+ # end
145
+ # end
146
+ #
147
+ # # Associations (eager load)
148
+ # class Product < ActiveRecord::Base
149
+ # belongs_to :shop
150
+ #
151
+ # sortable_by do |x|
152
+ # x.field :name
153
+ # x.field :shop, as: Shop.arel_table[:name], eager_load: :shop
154
+ # x.default 'shop,name'
155
+ # end
156
+ # end
157
+ #
158
+ # # Associations (custom scope)
159
+ # class Product < ActiveRecord::Base
160
+ # belongs_to :shop
161
+ #
162
+ # sortable_by do |x|
163
+ # x.field :shop, as: Shop.arel_table[:name], scope: -> { joins(:shop) }
164
+ # end
165
+ # end
166
+ #
167
+ def sortable_by(*attributes, **opts)
168
+ config = _sortable_by_config
169
+ default = opts.delete(:default)
170
+
171
+ attributes.each do |name|
172
+ config.field(name, **opts)
173
+ end
174
+ config.default(default) if default
175
+ yield config if block_given?
176
+ config
177
+ end
178
+
179
+ # @param [String] expr the sort expr
180
+ # @return [ActiveRecord::Relation] the scoped relation
181
+ def sorted_by(expr)
182
+ _sortable_by_config.send :order, self, expr
183
+ end
91
184
  end
92
185
 
93
186
  class Base # :nodoc:
94
- include SortableByHelper
187
+ extend SortableBy
95
188
  end
96
189
  end
@@ -1,7 +1,6 @@
1
-
2
1
  Gem::Specification.new do |s|
3
2
  s.name = 'sortable-by'
4
- s.version = '0.10.0'
3
+ s.version = '0.13.0'
5
4
  s.authors = ['Dimitrij Denissenko']
6
5
  s.email = ['dimitrij@blacksquaremedia.com']
7
6
  s.summary = 'Generate white-listed sort scopes from URL parameter values'
@@ -9,10 +8,10 @@ Gem::Specification.new do |s|
9
8
  s.homepage = 'https://github.com/bsm/sortable-by'
10
9
  s.license = 'MIT'
11
10
 
12
- s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^spec/}) }
11
+ s.files = `git ls-files -z`.split("\x0").reject {|f| f.match(%r{^spec/}) }
13
12
  s.test_files = `git ls-files -z -- spec/*`.split("\x0")
14
13
  s.require_paths = ['lib']
15
- s.required_ruby_version = '>= 2.2.0'
14
+ s.required_ruby_version = '>= 2.5'
16
15
 
17
16
  s.add_dependency 'activerecord'
18
17
  s.add_dependency 'activesupport'
@@ -1,28 +1,86 @@
1
- require File.dirname(__FILE__) + '/spec_helper'
1
+ require File.expand_path('./spec_helper', __dir__)
2
+
3
+ describe ActiveRecord::SortableBy do
4
+ before do
5
+ c = ActiveRecord::Base.connection
6
+ c.tables.each do |t|
7
+ c.update "DELETE FROM #{t}"
8
+ end
9
+ end
2
10
 
3
- describe ActiveRecord::SortableByHelper do
4
11
  it 'should have config' do
5
- expect(Foo._sortable_by_scope_options).to include(:default, :scopes)
6
- expect(Foo._sortable_by_scope_options[:default]).to eq(title: :asc)
7
- expect(Foo._sortable_by_scope_options[:scopes]).to be_instance_of(Hash)
8
- expect(Foo._sortable_by_scope_options[:scopes]).to include('title', 'age', 'semver', 'insensitive')
9
- expect(Bar._sortable_by_scope_options).to eq(default: { id: :asc }, scopes: {})
12
+ expect(Post._sortable_by_config._fields.keys).to match_array(%w[title created])
13
+ expect(Post._sortable_by_config._default).to eq('-created')
14
+ expect(SubPost._sortable_by_config._fields.keys).to match_array(%w[title created])
15
+ expect(SubPost._sortable_by_config._default).to eq('-created')
16
+
17
+ expect(App._sortable_by_config._fields.keys).to match_array(%w[name version])
18
+ expect(App._sortable_by_config._default).to eq('name')
19
+
20
+ expect(Shop._sortable_by_config._fields.keys).to be_empty
21
+ expect(Shop._sortable_by_config._default).to be_nil
22
+
23
+ expect(Product._sortable_by_config._fields.keys).to match_array(%w[name shop])
24
+ expect(Product._sortable_by_config._default).to eq('shop,name')
25
+ end
26
+
27
+ it 'should simply order' do
28
+ Post.create! title: 'A', created_at: Time.at(1515151500)
29
+ Post.create! title: 'b', created_at: Time.at(1515151600)
30
+ Post.create! title: 'C', created_at: Time.at(1515151400)
31
+
32
+ expect(Post.sorted_by(nil).pluck(:title)).to eq(%w[b A C])
33
+ expect(Post.sorted_by('').pluck(:title)).to eq(%w[b A C])
34
+ expect(Post.sorted_by('invalid').pluck(:title)).to eq(%w[b A C])
35
+
36
+ expect(Post.sorted_by('-created').pluck(:title)).to eq(%w[b A C])
37
+ expect(Post.sorted_by('created').pluck(:title)).to eq(%w[C A b])
38
+
39
+ expect(Post.sorted_by('title').pluck(:title)).to eq(%w[A C b])
40
+ expect(Post.sorted_by('-title').pluck(:title)).to eq(%w[b C A])
41
+ expect(Post.sorted_by(' title ').pluck(:title)).to eq(%w[A C b])
10
42
  end
11
43
 
12
- it 'should generate scopes' do
13
- expect(Foo.sorted_by('title').pluck(:title)).to eq(%w[A B C b])
14
- expect(Foo.sorted_by('-title').pluck(:title)).to eq(%w[b C B A])
15
- expect(Foo.sorted_by('age,title').pluck(:title)).to eq(%w[B A b C])
16
- expect(Foo.sorted_by('invalid , -age').pluck(:title)).to eq(%w[C b A B])
17
- expect(Foo.sorted_by('insensitive,age').pluck(:title)).to eq(%w[A B b C])
18
- expect(Foo.sorted_by('-insensitive,age').pluck(:title)).to eq(%w[C B b A])
19
- expect(Foo.sorted_by('semver').pluck(:title)).to eq(%w[b B C A])
20
- expect(Foo.sorted_by('-semver').pluck(:title)).to eq(%w[A C B b])
21
- expect(Foo.sorted_by('').pluck(:title)).to eq(%w[A B C b])
22
- expect(Foo.sorted_by(nil).pluck(:title)).to eq(%w[A B C b])
23
-
24
- expect(Bar.sorted_by('').pluck(:title)).to eq(%w[Y X])
25
- expect(Bar.sorted_by('title').pluck(:title)).to eq(%w[Y X])
26
- expect(Bar.where(title: 'X').sorted_by('title').pluck(:title)).to eq(%w[X])
44
+ it 'should support STI inheritance and overrides' do
45
+ SubPost.create! title: 'A', created_at: Time.at(1515151700)
46
+ SubPost.create! title: 'b', created_at: Time.at(1515151600)
47
+ Post.create! title: 'C', created_at: Time.at(1515151400)
48
+ SubPost.create! title: 'D', created_at: Time.at(1515151500)
49
+
50
+ expect(Post.sorted_by(nil).pluck(:title)).to eq(%w[A b D C])
51
+ expect(SubPost.sorted_by(nil).pluck(:title)).to eq(%w[A b D])
52
+ expect(SubPost.sorted_by('-created').pluck(:title)).to eq(%w[A b D])
53
+ expect(SubPost.sorted_by('created').pluck(:title)).to eq(%w[D b A])
54
+
55
+ expect(Post.sorted_by('title').pluck(:title)).to eq(%w[A C D b])
56
+ expect(SubPost.sorted_by('title').pluck(:title)).to eq(%w[A b D])
57
+ end
58
+
59
+ it 'should support composition' do
60
+ App.create! name: 'E', major: 0, minor: 9, patch: 2
61
+ App.create! name: 'A', major: 1, minor: 0, patch: 1
62
+ App.create! name: 'D', major: 1, minor: 0, patch: 6
63
+ App.create! name: 'C', major: 1, minor: 1, patch: 0
64
+ App.create! name: 'B', major: 2, minor: 2, patch: 0
65
+
66
+ expect(App.sorted_by(nil).pluck(:name)).to eq(%w[A B C D E])
67
+ expect(App.sorted_by('version').pluck(:name)).to eq(%w[E A D C B])
68
+ expect(App.sorted_by('-version').pluck(:name)).to eq(%w[B C D A E])
69
+ end
70
+
71
+ it 'should support associations' do
72
+ y = Shop.create! name: 'Y'
73
+ x = Shop.create! name: 'X'
74
+
75
+ Product.create! name: 'a', shop_id: y.id
76
+ Product.create! name: 'B', shop_id: y.id, active: false
77
+ Product.create! name: 'c', shop_id: x.id
78
+ Product.create! name: 'D', shop_id: y.id
79
+ Product.create! name: 'e', shop_id: x.id
80
+ Product.create! name: 'f', shop_id: x.id, active: false
81
+
82
+ expect(Product.sorted_by(nil).pluck(:name)).to eq(%w[c e f a B D])
83
+ expect(Product.where(active: true).sorted_by(nil).pluck(:name)).to eq(%w[c e a D])
84
+ expect(Product.sorted_by('name').pluck(:name)).to eq(%w[a B c D e f])
27
85
  end
28
86
  end
@@ -2,41 +2,71 @@ ENV['RACK_ENV'] ||= 'test'
2
2
  require 'sortable-by'
3
3
  require 'rspec'
4
4
 
5
- ActiveRecord::Base.configurations['test'] = {
6
- 'adapter' => 'sqlite3',
7
- 'database' => ':memory:',
5
+ ActiveRecord::Base.configurations = {
6
+ 'test' => {
7
+ 'adapter' => 'sqlite3',
8
+ 'database' => ':memory:',
9
+ },
8
10
  }
9
11
  ActiveRecord::Base.establish_connection :test
10
- ActiveRecord::Base.connection.create_table :foos do |t|
11
- t.string :title
12
- t.integer :age
13
12
 
14
- t.integer :major
15
- t.integer :minor
13
+ ActiveRecord::Base.connection.create_table :posts do |t|
14
+ t.string :type
15
+ t.string :title, null: false
16
+ t.timestamp :created_at, null: false
16
17
  end
17
18
 
18
- ActiveRecord::Base.connection.create_table :bars do |t|
19
- t.string :title
19
+ class Post < ActiveRecord::Base
20
+ sortable_by :title, default: '-created', case_sensitive: true do |s|
21
+ s.field :created, as: arel_table[:created_at]
22
+ end
20
23
  end
21
24
 
22
- ActiveRecord::Base.connection.create_table :boos do |t|
23
- t.string :title
25
+ class SubPost < Post
26
+ sortable_by do |s|
27
+ s.field :title
28
+ end
24
29
  end
25
30
 
26
- class Foo < ActiveRecord::Base
27
- sortable_by :title, :age,
28
- insensitive: Arel::Nodes::NamedFunction.new('LOWER', [arel_table[:title]]),
29
- semver: %i[major minor],
30
- default: { title: :asc }
31
+ # ---------------------------------------------------------------------
32
+
33
+ ActiveRecord::Base.connection.create_table :apps do |t|
34
+ t.string :name, null: false
35
+ t.integer :major, null: false
36
+ t.integer :minor, null: false
37
+ t.integer :patch, null: false
38
+ end
39
+
40
+ class App < ActiveRecord::Base
41
+ sortable_by :name do |s|
42
+ s.field :version, as: %i[major minor patch]
43
+ end
44
+ end
45
+
46
+ # ---------------------------------------------------------------------
47
+
48
+ ActiveRecord::Base.connection.create_table :shops do |t|
49
+ t.string :name, null: false
31
50
  end
32
51
 
33
- class Bar < ActiveRecord::Base
52
+ class Shop < ActiveRecord::Base
34
53
  end
35
54
 
36
- Foo.create! title: 'A', age: 25, major: 10, minor: 1
37
- Foo.create! title: 'B', age: 24, major: 8, minor: 3
38
- Foo.create! title: 'b', age: 26, major: 0, minor: 2
39
- Foo.create! title: 'C', age: 27, major: 8, minor: 6
55
+ # ---------------------------------------------------------------------
56
+
57
+ ActiveRecord::Base.connection.create_table :products do |t|
58
+ t.string :name, null: false
59
+ t.integer :shop_id, null: false
60
+ t.boolean :active, null: false, default: true
61
+ t.foreign_key :shops
62
+ end
40
63
 
41
- Bar.create! title: 'Y'
42
- Bar.create! title: 'X'
64
+ class Product < ActiveRecord::Base
65
+ belongs_to :shop
66
+
67
+ sortable_by do |s|
68
+ s.field :name
69
+ s.field :shop, as: Shop.arel_table[:name], eager_load: :shop
70
+ s.default 'shop,name'
71
+ end
72
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sortable-by
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dimitrij Denissenko
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-03-19 00:00:00.000000000 Z
11
+ date: 2020-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -140,15 +140,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
140
140
  requirements:
141
141
  - - ">="
142
142
  - !ruby/object:Gem::Version
143
- version: 2.2.0
143
+ version: '2.5'
144
144
  required_rubygems_version: !ruby/object:Gem::Requirement
145
145
  requirements:
146
146
  - - ">="
147
147
  - !ruby/object:Gem::Version
148
148
  version: '0'
149
149
  requirements: []
150
- rubyforge_project:
151
- rubygems_version: 2.7.3
150
+ rubygems_version: 3.1.2
152
151
  signing_key:
153
152
  specification_version: 4
154
153
  summary: Generate white-listed sort scopes from URL parameter values