sortable-by 0.10.0 → 0.13.0

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