sortable-by 0.10.0 → 0.11.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: 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