sortable-by 0.10.0 → 0.11.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: bc4cdc4741e458addb43e7d95a66f12695cf601f4178d188de2fa5197d6dc199
4
+ data.tar.gz: b1c4bb549dcdea2ca6879d5c4e360b75d099171ed2a3b1bd4e28fa89e0b968d9
5
5
  SHA512:
6
- metadata.gz: 68712cabc1e8d2bc54d4716d27cd896170fad207448814a0c784f7a018bd2da8b0fad7d26ba1cfca8cb884969665f014c3aa0912c457fbb2465a1fa4c7214c5c
7
- data.tar.gz: 9d9b0f42b50c94cbcd36eec6cb2b1c1367150943a6c252092d84d45b3aecdb5ea1431ff227faa68db045b32fc02983f4d0bcb41fa1619a9f28d4a1ffd1789666
6
+ metadata.gz: 381a3fbb778b6540e44f76de77b733781f26686848342235b865608bcaa5aaa5878bbf184de2386570568642c637d419ffe7a68eb980ecd7e084eaf8564ad98c
7
+ data.tar.gz: 705e4e7a6eae3f496ae2e84f3d2f818c4a0c2341d2e93bdb7ebde678bbc32d0f8b7c4375447168025c222bd466dde5ba72cd2624690ef5c5c99c03cb66c094e3
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
1
  spec/tmp
2
2
  pkg
3
3
  *.gem
4
+ .rake_tasks~
data/.rubocop.yml CHANGED
@@ -1,15 +1,26 @@
1
1
  AllCops:
2
2
  TargetRubyVersion: 2.2
3
3
 
4
+ Naming/MemoizedInstanceVariableName:
5
+ Enabled: false
6
+
4
7
  Metrics/AbcSize:
5
8
  Enabled: false
9
+ Metrics/CyclomaticComplexity:
10
+ Enabled: false
11
+ Metrics/PerceivedComplexity:
12
+ Enabled: false
6
13
  Naming/FileName:
7
14
  Exclude: [lib/sortable-by.rb]
15
+ Metrics/BlockLength:
16
+ Exclude: [spec/**]
8
17
  Metrics/MethodLength:
9
- Max: 20
18
+ Enabled: false
10
19
  Metrics/LineLength:
11
20
  Max: 120
12
21
 
22
+ Style/NumericLiterals:
23
+ Exclude: [spec/**]
13
24
  Style/TrailingCommaInArguments:
14
25
  EnforcedStyleForMultiline: consistent_comma
15
26
  Style/TrailingCommaInArrayLiteral:
data/.travis.yml CHANGED
@@ -3,6 +3,5 @@ rvm:
3
3
  - 2.2
4
4
  - 2.3
5
5
  - 2.4
6
- - 2.5
7
6
  gemfile:
8
7
  - Gemfile
data/Gemfile.lock CHANGED
@@ -1,37 +1,37 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sortable-by (0.10.0)
4
+ sortable-by (0.11.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 (5.2.0)
12
+ activesupport (= 5.2.0)
13
+ activerecord (5.2.0)
14
+ activemodel (= 5.2.0)
15
+ activesupport (= 5.2.0)
16
+ arel (>= 9.0)
17
+ activesupport (5.2.0)
18
18
  concurrent-ruby (~> 1.0, >= 1.0.2)
19
- i18n (~> 0.7)
19
+ i18n (>= 0.7, < 2)
20
20
  minitest (~> 5.1)
21
21
  tzinfo (~> 1.1)
22
- arel (8.0.0)
22
+ arel (9.0.0)
23
23
  ast (2.4.0)
24
24
  concurrent-ruby (1.0.5)
25
25
  diff-lcs (1.3)
26
- i18n (0.9.5)
26
+ i18n (1.0.0)
27
27
  concurrent-ruby (~> 1.0)
28
28
  minitest (5.11.3)
29
29
  parallel (1.12.1)
30
- parser (2.5.0.4)
30
+ parser (2.5.0.5)
31
31
  ast (~> 2.4.0)
32
32
  powerpack (0.1.1)
33
33
  rainbow (3.0.0)
34
- rake (12.3.0)
34
+ rake (12.3.1)
35
35
  rspec (3.7.0)
36
36
  rspec-core (~> 3.7.0)
37
37
  rspec-expectations (~> 3.7.0)
@@ -45,7 +45,7 @@ GEM
45
45
  diff-lcs (>= 1.2.0, < 2.0)
46
46
  rspec-support (~> 3.7.0)
47
47
  rspec-support (3.7.1)
48
- rubocop (0.53.0)
48
+ rubocop (0.54.0)
49
49
  parallel (~> 1.10)
50
50
  parser (>= 2.5)
51
51
  powerpack (~> 0.1)
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 posts.title ASC
24
+ Post.sorted_by('-title') # => ORDER BY posts.title DESC
25
+ Post.sorted_by('bad,title') # => ORDER BY posts.title ASC
26
+ Post.sorted_by(nil) # => ORDER BY posts.title ASC
26
27
  ```
27
28
 
28
- Aliases and composition:
29
+ Case-insensitive:
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, as: arel_table[:title].lower
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 LOWER(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 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, as: arel_table[:name].lower
72
+ x.field :shop, as: Shop.arel_table[:name].lower, eager_load: :shop
73
+ x.default 'shop,name'
74
+ end
75
+ end
76
+ ```
77
+
78
+ Associations (custom scope):
79
+
80
+ ```
81
+ class Product < ActiveRecord::Base
82
+ belongs_to :shop
83
+ sortable_by do |x|
84
+ x.field :shop, as: Shop.arel_table[:name].lower, 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]
data/lib/sortable_by.rb CHANGED
@@ -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,142 @@ 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, false) if fallback && !matched && _default
58
+ relation
59
+ end
60
+ end
61
+
62
+ class Field # :nodoc:
63
+ def initialize(name, opts = {})
64
+ @cols = Array.wrap(opts[:as])
65
+ @eager_load = Array.wrap(opts[:eager_load]).presence
66
+
67
+ # validate custom_scope
68
+ @custom_scope = opts[:scope]
69
+ if @custom_scope && !@custom_scope.is_a?(Proc)
70
+ raise ArgumentError, "Invalid sortable-by field '#{name}': scope must be a Proc."
86
71
  end
87
72
 
73
+ # normalize cols
74
+ @cols.push name if @cols.empty?
75
+ @cols.each do |col|
76
+ case col
77
+ when String, Symbol, Arel::Attributes::Attribute, Arel::Nodes::Node
78
+ next
79
+ when Proc
80
+ unless col.arity == 2
81
+ raise ArgumentError, "Invalid sortable-by field '#{name}': proc must accept 2 arguments."
82
+ end
83
+ else
84
+ raise ArgumentError, "Invalid sortable-by field '#{name}': invalid type #{col.class}."
85
+ end
86
+ end
87
+ end
88
+
89
+ def order(relation, rank)
90
+ @cols.each do |col|
91
+ case col
92
+ when String, Symbol
93
+ relation = relation.order(col => rank)
94
+ when Arel::Nodes::Node, Arel::Attributes::Attribute
95
+ relation = relation.order(col.send(rank))
96
+ when Proc
97
+ relation = col.call(relation, rank)
98
+ end
99
+ end
100
+
101
+ relation = relation.eager_load(*@eager_load) if @eager_load
102
+ relation = relation.instance_eval(&@custom_scope) if @custom_scope
88
103
  relation
89
104
  end
90
105
  end
106
+
107
+ def self.extended(base) # :nodoc:
108
+ base.class_attribute :_sortable_by_config, instance_accessor: false, instance_predicate: false
109
+ base._sortable_by_config = Config.new
110
+ end
111
+
112
+ def inherited(base) # :nodoc:
113
+ base._sortable_by_config = _sortable_by_config.deep_dup
114
+ super
115
+ end
116
+
117
+ # Declare sortable attributes and scopes. Examples:
118
+ #
119
+ # # Simple
120
+ # class Post < ActiveRecord::Base
121
+ # sortable_by :title, :id
122
+ # end
123
+ #
124
+ # # Case-insensitive
125
+ # class Post < ActiveRecord::Base
126
+ # sortable_by do |x|
127
+ # x.field :title, as: arel_table[:title].lower
128
+ # x.field :id
129
+ # end
130
+ # end
131
+ #
132
+ # # With default
133
+ # class Post < ActiveRecord::Base
134
+ # sortable_by :id, :topic, :created_at,
135
+ # default: 'topic,-created_at'
136
+ # end
137
+ #
138
+ # # Composition
139
+ # class App < ActiveRecord::Base
140
+ # sortable_by :name, default: '-version' do |x|
141
+ # x.field :version, as: %i[major minor patch]]
142
+ # end
143
+ # end
144
+ #
145
+ # # Associations (eager load)
146
+ # class Product < ActiveRecord::Base
147
+ # belongs_to :shop
148
+ #
149
+ # sortable_by do |x|
150
+ # x.field :name
151
+ # x.field :shop, as: Shop.arel_table[:name].lower, eager_load: :shop
152
+ # x.default 'shop,name'
153
+ # end
154
+ # end
155
+ #
156
+ # # Associations (custom scope)
157
+ # class Product < ActiveRecord::Base
158
+ # belongs_to :shop
159
+ #
160
+ # sortable_by do |x|
161
+ # x.field :shop, as: Shop.arel_table[:name].lower, scope: -> { joins(:shop) }
162
+ # end
163
+ # end
164
+ #
165
+ def sortable_by(*attributes)
166
+ config = _sortable_by_config
167
+ opts = attributes.extract_options!
168
+ default = opts.delete(:default)
169
+
170
+ attributes.each do |name|
171
+ config.field(name, opts)
172
+ end
173
+ config.default(default) if default
174
+ yield config if block_given?
175
+ config
176
+ end
177
+
178
+ # @param [String] expr the sort expr
179
+ # @return [ActiveRecord::Relation] the scoped relation
180
+ def sorted_by(expr)
181
+ _sortable_by_config.send :order, self, expr
182
+ end
91
183
  end
92
184
 
93
185
  class Base # :nodoc:
94
- include SortableByHelper
186
+ extend SortableBy
95
187
  end
96
188
  end
data/sortable-by.gemspec CHANGED
@@ -1,7 +1,7 @@
1
1
 
2
2
  Gem::Specification.new do |s|
3
3
  s.name = 'sortable-by'
4
- s.version = '0.10.0'
4
+ s.version = '0.11.0'
5
5
  s.authors = ['Dimitrij Denissenko']
6
6
  s.email = ['dimitrij@blacksquaremedia.com']
7
7
  s.summary = 'Generate white-listed sort scopes from URL parameter values'
@@ -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
data/spec/spec_helper.rb CHANGED
@@ -7,36 +7,64 @@ ActiveRecord::Base.configurations['test'] = {
7
7
  'database' => ':memory:',
8
8
  }
9
9
  ActiveRecord::Base.establish_connection :test
10
- ActiveRecord::Base.connection.create_table :foos do |t|
11
- t.string :title
12
- t.integer :age
13
10
 
14
- t.integer :major
15
- t.integer :minor
11
+ ActiveRecord::Base.connection.create_table :posts do |t|
12
+ t.string :type
13
+ t.string :title, null: false
14
+ t.timestamp :created_at, null: false
16
15
  end
17
16
 
18
- ActiveRecord::Base.connection.create_table :bars do |t|
19
- t.string :title
17
+ class Post < ActiveRecord::Base
18
+ sortable_by :title, default: '-created' do |s|
19
+ s.field :created, as: arel_table[:created_at]
20
+ end
20
21
  end
21
22
 
22
- ActiveRecord::Base.connection.create_table :boos do |t|
23
- t.string :title
23
+ class SubPost < Post
24
+ sortable_by do |s|
25
+ s.field :title, as: arel_table[:title].lower
26
+ end
24
27
  end
25
28
 
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 }
29
+ # ---------------------------------------------------------------------
30
+
31
+ ActiveRecord::Base.connection.create_table :apps do |t|
32
+ t.string :name, null: false
33
+ t.integer :major, null: false
34
+ t.integer :minor, null: false
35
+ t.integer :patch, null: false
36
+ end
37
+
38
+ class App < ActiveRecord::Base
39
+ sortable_by :name do |s|
40
+ s.field :version, as: %i[major minor patch]
41
+ end
42
+ end
43
+
44
+ # ---------------------------------------------------------------------
45
+
46
+ ActiveRecord::Base.connection.create_table :shops do |t|
47
+ t.string :name, null: false
31
48
  end
32
49
 
33
- class Bar < ActiveRecord::Base
50
+ class Shop < ActiveRecord::Base
34
51
  end
35
52
 
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
53
+ # ---------------------------------------------------------------------
54
+
55
+ ActiveRecord::Base.connection.create_table :products do |t|
56
+ t.string :name, null: false
57
+ t.integer :shop_id, null: false
58
+ t.boolean :active, null: false, default: true
59
+ t.foreign_key :shops
60
+ end
40
61
 
41
- Bar.create! title: 'Y'
42
- Bar.create! title: 'X'
62
+ class Product < ActiveRecord::Base
63
+ belongs_to :shop
64
+
65
+ sortable_by do |s|
66
+ s.field :name, as: arel_table[:name].lower
67
+ s.field :shop, as: Shop.arel_table[:name].lower, eager_load: :shop
68
+ s.default 'shop,name'
69
+ end
70
+ 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.11.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: 2018-04-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord