actionset 0.8.0 → 0.8.1
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 +4 -4
- data/CHANGELOG +17 -0
- data/Gemfile.lock +1 -1
- data/actionset.gemspec +1 -1
- data/lib/action_set/helpers/pagination/current_page_description_for_helper.rb +6 -1
- data/lib/action_set/helpers/pagination/total_pages_for_helper.rb +1 -1
- data/lib/active_set/active_record_set_instruction.rb +67 -0
- data/lib/active_set/attribute_instruction.rb +41 -13
- data/lib/active_set/enumerable_set_instruction.rb +46 -0
- data/lib/active_set/filtering/active_record_strategy.rb +32 -45
- data/lib/active_set/filtering/enumerable_strategy.rb +46 -45
- data/lib/active_set/sorting/active_record_strategy.rb +16 -32
- data/lib/active_set/sorting/enumerable_strategy.rb +9 -16
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2d8befffeaabf8526df7cdd15f501f2e9e99732b
|
4
|
+
data.tar.gz: 9dc431b8b1af60ed1d6d57d8ba9e66ac6e6a0688
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 076e19f851950c2a0c2ac377c51a04eaf16198060ce8a1e6a766339f659c61eab1ae125c34005db809b27b0b0c02aad85e9ad1502f13c11ad04a821be6d40d0d
|
7
|
+
data.tar.gz: c2de49f8fb3a36ecf86dfc00c51a880b675ea8f7db129de84a0eace0565c9175528cd53fcf25096ff04151c727189ce09c53ac54b50221df20784e8ba8e52270
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,20 @@
|
|
1
|
+
v 0.8.1
|
2
|
+
- add a case-insensitive filtering/sorting option (`/i/`)
|
3
|
+
- standardize how filtering by class scopes behaves
|
4
|
+
+ only collection-returning class methods are handled
|
5
|
+
+ scopes defined on computed associations aren't handled
|
6
|
+
- refactor parts of the strategy implemention layer
|
7
|
+
+ create generalize "set instruction" classes for ActiveRecord and Enumerable strategies
|
8
|
+
+ handle default operators in the strategy layer, not the instruction layer
|
9
|
+
- refactor parts of the instruction implementation layer
|
10
|
+
+ memoize instruction getters to avoid recomputing on access
|
11
|
+
- refactor specs
|
12
|
+
+ update filtering by data types when using an invalid field test
|
13
|
+
+ remove case-insensitive sorting example
|
14
|
+
+ simplify the sorted collection expectation
|
15
|
+
+ add some helper methods for generating unique factory data
|
16
|
+
+ print a detailed report of the state data on spec failure
|
17
|
+
+ update the filtering request specs to run more of then, but still running random subsets
|
1
18
|
v 0.8.0
|
2
19
|
- merge ActiveSet and ActionSet into a monorepo
|
3
20
|
- merge and overhaul the specs
|
data/Gemfile.lock
CHANGED
data/actionset.gemspec
CHANGED
@@ -6,7 +6,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.platform = Gem::Platform::RUBY
|
8
8
|
spec.name = 'actionset'
|
9
|
-
spec.version = '0.8.
|
9
|
+
spec.version = '0.8.1'
|
10
10
|
spec.authors = ['Stephen Margheim']
|
11
11
|
spec.email = ['stephen.margheim@gmail.com']
|
12
12
|
|
@@ -9,7 +9,12 @@ module Pagination
|
|
9
9
|
include Pagination::TotalPagesForHelper
|
10
10
|
|
11
11
|
def pagination_current_page_description_for(set)
|
12
|
-
description =
|
12
|
+
description = [
|
13
|
+
'Page',
|
14
|
+
"<strong>#{pagination_current_page_for(set)}</strong>",
|
15
|
+
'of',
|
16
|
+
"<strong>#{pagination_total_pages_for(set)}</strong>"
|
17
|
+
].join(' ').html_safe
|
13
18
|
|
14
19
|
content_tag(:span,
|
15
20
|
description,
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ActiveSet
|
4
|
+
class ActiveRecordSetInstruction < SimpleDelegator
|
5
|
+
def initialize(attribute_instruction, set)
|
6
|
+
@attribute_instruction = attribute_instruction
|
7
|
+
@set = set
|
8
|
+
super(@attribute_instruction)
|
9
|
+
end
|
10
|
+
|
11
|
+
def initial_relation
|
12
|
+
return @set if @attribute_instruction.associations_array.empty?
|
13
|
+
|
14
|
+
@set.eager_load(@attribute_instruction.associations_hash)
|
15
|
+
end
|
16
|
+
|
17
|
+
def arel_type
|
18
|
+
attribute_model
|
19
|
+
.columns_hash[@attribute_instruction.attribute]
|
20
|
+
.type
|
21
|
+
end
|
22
|
+
|
23
|
+
def arel_table
|
24
|
+
# This is to work around an bug in ActiveRecord,
|
25
|
+
# where BINARY fields aren't found properly when using
|
26
|
+
# the `arel_table` class method to build an ARel::Node
|
27
|
+
if arel_type == :binary
|
28
|
+
Arel::Table.new(attribute_model.table_name)
|
29
|
+
else
|
30
|
+
attribute_model.arel_table
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def arel_column
|
35
|
+
_arel_column = arel_table[@attribute_instruction.attribute]
|
36
|
+
return _arel_column.lower if case_insensitive_operation?
|
37
|
+
|
38
|
+
_arel_column
|
39
|
+
end
|
40
|
+
|
41
|
+
def arel_operator
|
42
|
+
@attribute_instruction.operator(default: :eq)
|
43
|
+
end
|
44
|
+
|
45
|
+
def arel_value
|
46
|
+
_arel_value = @attribute_instruction.value
|
47
|
+
return _arel_value.downcase if case_insensitive_operation?
|
48
|
+
|
49
|
+
_arel_value
|
50
|
+
end
|
51
|
+
|
52
|
+
def case_insensitive_operation?
|
53
|
+
@attribute_instruction.case_insensitive? && arel_type.presence_in(%i[string text])
|
54
|
+
end
|
55
|
+
|
56
|
+
def attribute_model
|
57
|
+
return @set.klass if @attribute_instruction.associations_array.empty?
|
58
|
+
return @attribute_model if defined? @attribute_model
|
59
|
+
|
60
|
+
@attribute_model = @attribute_instruction
|
61
|
+
.associations_array
|
62
|
+
.reduce(@set) do |obj, assoc|
|
63
|
+
obj.reflections[assoc.to_s]&.klass
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -17,36 +17,56 @@ class ActiveSet
|
|
17
17
|
@processed
|
18
18
|
end
|
19
19
|
|
20
|
+
def case_insensitive?
|
21
|
+
return false unless options
|
22
|
+
|
23
|
+
options.include? :i
|
24
|
+
end
|
25
|
+
|
20
26
|
def attribute
|
21
|
-
attribute
|
22
|
-
return attribute.sub(operator_regex, '') if attribute&.match operator_regex
|
27
|
+
return @attribute if defined? @attribute
|
23
28
|
|
24
|
-
attribute
|
29
|
+
attribute = @keypath.last
|
30
|
+
attribute = attribute&.sub(operator_regex, '')
|
31
|
+
attribute = attribute&.sub(options_regex, '')
|
32
|
+
@attribute = attribute
|
25
33
|
end
|
26
34
|
|
27
35
|
def operator(default: '==')
|
28
|
-
|
29
|
-
|
36
|
+
return @operator if defined? @operator
|
37
|
+
|
38
|
+
attribute_instruction = @keypath.last
|
39
|
+
@operator = (attribute_instruction[operator_regex, 1] || default).to_sym
|
40
|
+
end
|
41
|
+
|
42
|
+
def options
|
43
|
+
return @options if defined? @options
|
30
44
|
|
31
|
-
|
45
|
+
@options = @keypath.last[options_regex, 1]&.split('')&.map(&:to_sym)
|
32
46
|
end
|
33
47
|
|
34
48
|
def associations_array
|
49
|
+
return @associations_array if defined? @associations_array
|
35
50
|
return [] unless @keypath.any?
|
36
51
|
|
37
|
-
@keypath.slice(0, @keypath.length - 1)
|
52
|
+
@associations_array = @keypath.slice(0, @keypath.length - 1)
|
38
53
|
end
|
39
54
|
|
40
55
|
def associations_hash
|
56
|
+
return @associations_hash if defined? @associations_hash
|
41
57
|
return {} unless @keypath.any?
|
42
58
|
|
43
|
-
associations_array.reverse.reduce({}) do |hash, association|
|
59
|
+
@associations_hash = associations_array.reverse.reduce({}) do |hash, association|
|
44
60
|
{ association => hash }
|
45
61
|
end
|
46
62
|
end
|
47
63
|
|
48
64
|
def value_for(item:)
|
49
|
-
|
65
|
+
@values_for ||= Hash.new do |h, key|
|
66
|
+
h[key] = resource_for(item: key).public_send(attribute)
|
67
|
+
end
|
68
|
+
|
69
|
+
@values_for[item]
|
50
70
|
rescue StandardError
|
51
71
|
# :nocov:
|
52
72
|
nil
|
@@ -54,11 +74,15 @@ class ActiveSet
|
|
54
74
|
end
|
55
75
|
|
56
76
|
def resource_for(item:)
|
57
|
-
|
58
|
-
|
77
|
+
@resources_for ||= Hash.new do |h, key|
|
78
|
+
h[key] = associations_array.reduce(key) do |resource, association|
|
79
|
+
break nil unless resource.respond_to? association
|
59
80
|
|
60
|
-
|
81
|
+
resource.public_send(association)
|
82
|
+
end
|
61
83
|
end
|
84
|
+
|
85
|
+
@resources_for[item]
|
62
86
|
rescue StandardError
|
63
87
|
# :nocov:
|
64
88
|
nil
|
@@ -68,7 +92,11 @@ class ActiveSet
|
|
68
92
|
private
|
69
93
|
|
70
94
|
def operator_regex
|
71
|
-
|
95
|
+
%r{\((.*?)\)}
|
96
|
+
end
|
97
|
+
|
98
|
+
def options_regex
|
99
|
+
%r{\/(.*?)\/}
|
72
100
|
end
|
73
101
|
end
|
74
102
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ActiveSet
|
4
|
+
class EnumerableSetInstruction < SimpleDelegator
|
5
|
+
def initialize(attribute_instruction, set)
|
6
|
+
@attribute_instruction = attribute_instruction
|
7
|
+
@set = set
|
8
|
+
super(@attribute_instruction)
|
9
|
+
end
|
10
|
+
|
11
|
+
def attribute_value_for(item)
|
12
|
+
item_value = @attribute_instruction
|
13
|
+
.value_for(item: item)
|
14
|
+
item_value = item_value.downcase if case_insensitive_operation_for?(item_value)
|
15
|
+
item_value
|
16
|
+
end
|
17
|
+
|
18
|
+
def attribute_value
|
19
|
+
_attribute_value = @attribute_instruction.value
|
20
|
+
_attribute_value = _attribute_value.downcase if case_insensitive_operation_for?(_attribute_value)
|
21
|
+
_attribute_value
|
22
|
+
end
|
23
|
+
|
24
|
+
def case_insensitive_operation_for?(value)
|
25
|
+
return false unless @attribute_instruction.case_insensitive?
|
26
|
+
|
27
|
+
value.is_a?(String) || value.is_a?(Symbol)
|
28
|
+
end
|
29
|
+
|
30
|
+
def attribute_instance
|
31
|
+
set_item = @set.find(&:present?)
|
32
|
+
return set_item if @attribute_instruction.associations_array.empty?
|
33
|
+
return @attribute_model if defined? @attribute_model
|
34
|
+
|
35
|
+
@attribute_model = @attribute_instruction
|
36
|
+
.associations_array
|
37
|
+
.reduce(set_item) do |obj, assoc|
|
38
|
+
obj.public_send(assoc)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def attribute_class
|
43
|
+
attribute_instance&.class
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -1,21 +1,34 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative '../active_record_set_instruction'
|
4
|
+
require 'active_support/core_ext/module/delegation'
|
5
|
+
|
3
6
|
class ActiveSet
|
4
7
|
module Filtering
|
5
8
|
class ActiveRecordStrategy
|
9
|
+
delegate :attribute_model,
|
10
|
+
:arel_column,
|
11
|
+
:arel_operator,
|
12
|
+
:arel_value,
|
13
|
+
:arel_type,
|
14
|
+
:initial_relation,
|
15
|
+
:attribute,
|
16
|
+
to: :@set_instruction
|
17
|
+
|
6
18
|
def initialize(set, attribute_instruction)
|
7
19
|
@set = set
|
8
20
|
@attribute_instruction = attribute_instruction
|
21
|
+
@set_instruction = ActiveRecordSetInstruction.new(attribute_instruction, set)
|
9
22
|
end
|
10
23
|
|
11
24
|
def execute
|
12
25
|
return false unless @set.respond_to? :to_sql
|
13
26
|
|
14
|
-
if
|
15
|
-
statement =
|
16
|
-
elsif
|
27
|
+
if execute_filter_operation?
|
28
|
+
statement = filter_operation
|
29
|
+
elsif execute_intersect_operation?
|
17
30
|
begin
|
18
|
-
statement =
|
31
|
+
statement = intersect_operation
|
19
32
|
rescue ArgumentError # thrown if merging a non-ActiveRecord::Relation
|
20
33
|
return false
|
21
34
|
end
|
@@ -28,71 +41,45 @@ class ActiveSet
|
|
28
41
|
|
29
42
|
private
|
30
43
|
|
31
|
-
def
|
44
|
+
def execute_filter_operation?
|
32
45
|
return false unless attribute_model
|
33
46
|
return false unless attribute_model.respond_to?(:attribute_names)
|
34
|
-
return false unless attribute_model.attribute_names.include?(
|
47
|
+
return false unless attribute_model.attribute_names.include?(attribute)
|
35
48
|
|
36
49
|
true
|
37
50
|
end
|
38
51
|
|
39
|
-
def
|
52
|
+
def execute_intersect_operation?
|
40
53
|
return false unless attribute_model
|
41
|
-
return false unless attribute_model.respond_to?(
|
42
|
-
return false if attribute_model.method(
|
54
|
+
return false unless attribute_model.respond_to?(attribute)
|
55
|
+
return false if attribute_model.method(attribute).arity.zero?
|
43
56
|
|
44
57
|
true
|
45
58
|
end
|
46
59
|
|
47
|
-
def
|
60
|
+
def filter_operation
|
48
61
|
initial_relation
|
49
62
|
.where(
|
50
63
|
arel_column.send(
|
51
|
-
|
52
|
-
|
64
|
+
arel_operator,
|
65
|
+
arel_value
|
53
66
|
)
|
54
67
|
)
|
55
68
|
end
|
56
69
|
|
57
|
-
def
|
70
|
+
def intersect_operation
|
71
|
+
# NOTE: If merging relations that contain duplicate column conditions,
|
72
|
+
# the second condition will replace the first.
|
73
|
+
# e.g. Thing.where(id: [1,2]).merge(Thing.where(id: [2,3]))
|
74
|
+
# => [Thing<2>, Thing<3>] NOT [Thing<2>]
|
58
75
|
initial_relation
|
59
76
|
.merge(
|
60
77
|
attribute_model.public_send(
|
61
|
-
|
62
|
-
|
78
|
+
attribute,
|
79
|
+
arel_value
|
63
80
|
)
|
64
81
|
)
|
65
82
|
end
|
66
|
-
|
67
|
-
def initial_relation
|
68
|
-
return @set if @attribute_instruction.associations_array.empty?
|
69
|
-
|
70
|
-
@set.eager_load(@attribute_instruction.associations_hash)
|
71
|
-
end
|
72
|
-
|
73
|
-
def arel_column
|
74
|
-
attribute_type = attribute_model.columns_hash[@attribute_instruction.attribute].type
|
75
|
-
|
76
|
-
# This is to work around an bug in ActiveRecord,
|
77
|
-
# where BINARY fields aren't found properly when using the `arel_table` class method
|
78
|
-
# to build an ARel::Node
|
79
|
-
if attribute_type == :binary
|
80
|
-
Arel::Table.new(attribute_model.table_name)[@attribute_instruction.attribute]
|
81
|
-
else
|
82
|
-
attribute_model.arel_table[@attribute_instruction.attribute]
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
def attribute_model
|
87
|
-
return @set.klass if @attribute_instruction.associations_array.empty?
|
88
|
-
return @attribute_model if defined? @attribute_model
|
89
|
-
|
90
|
-
@attribute_model = @attribute_instruction
|
91
|
-
.associations_array
|
92
|
-
.reduce(@set) do |obj, assoc|
|
93
|
-
obj.reflections[assoc.to_s]&.klass
|
94
|
-
end
|
95
|
-
end
|
96
83
|
end
|
97
84
|
end
|
98
85
|
end
|
@@ -1,77 +1,78 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative '../enumerable_set_instruction'
|
4
|
+
require 'active_support/core_ext/module/delegation'
|
5
|
+
|
3
6
|
class ActiveSet
|
4
7
|
module Filtering
|
5
8
|
class EnumerableStrategy
|
9
|
+
delegate :attribute_instance,
|
10
|
+
:attribute_class,
|
11
|
+
:attribute_value,
|
12
|
+
:attribute_value_for,
|
13
|
+
:operator,
|
14
|
+
:attribute,
|
15
|
+
to: :@set_instruction
|
16
|
+
|
6
17
|
def initialize(set, attribute_instruction)
|
7
18
|
@set = set
|
8
19
|
@attribute_instruction = attribute_instruction
|
20
|
+
@set_instruction = EnumerableSetInstruction.new(attribute_instruction, set)
|
9
21
|
end
|
10
22
|
|
11
23
|
def execute
|
12
24
|
return false unless @set.respond_to? :select
|
13
25
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
26
|
+
if execute_filter_operation?
|
27
|
+
set = filter_operation
|
28
|
+
elsif execute_intersect_operation?
|
29
|
+
begin
|
30
|
+
set = intersect_operation
|
31
|
+
rescue TypeError # thrown if intersecting with a non-Array
|
32
|
+
return false
|
33
|
+
end
|
34
|
+
else
|
35
|
+
return false
|
19
36
|
end
|
37
|
+
|
38
|
+
set
|
20
39
|
end
|
21
40
|
|
22
41
|
private
|
23
42
|
|
24
|
-
def
|
25
|
-
|
26
|
-
|
27
|
-
return false
|
28
|
-
return false unless attribute_item.respond_to?(@attribute_instruction.attribute)
|
29
|
-
return false if attribute_item.method(@attribute_instruction.attribute).arity.positive?
|
43
|
+
def execute_filter_operation?
|
44
|
+
return false unless attribute_instance
|
45
|
+
return false unless attribute_instance.respond_to?(attribute)
|
46
|
+
return false if attribute_instance.method(attribute).arity.positive?
|
30
47
|
|
31
48
|
true
|
32
49
|
end
|
33
50
|
|
34
|
-
def
|
35
|
-
|
36
|
-
|
37
|
-
return false
|
38
|
-
return false unless attribute_item.class
|
39
|
-
return false unless attribute_item.class.respond_to?(@attribute_instruction.attribute)
|
40
|
-
return false if attribute_item.class.method(@attribute_instruction.attribute).arity.zero?
|
51
|
+
def execute_intersect_operation?
|
52
|
+
return false unless attribute_class
|
53
|
+
return false unless attribute_class.respond_to?(attribute)
|
54
|
+
return false if attribute_class.method(attribute).arity.zero?
|
41
55
|
|
42
56
|
true
|
43
57
|
end
|
44
58
|
|
45
|
-
def
|
46
|
-
@
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
end
|
53
|
-
|
54
|
-
# rubocop:disable Metrics/MethodLength
|
55
|
-
def class_method_matches_for?(item)
|
56
|
-
maybe_item_or_collection_or_nil = attribute_item_for(item)
|
57
|
-
.class
|
58
|
-
.public_send(
|
59
|
-
@attribute_instruction.attribute,
|
60
|
-
@attribute_instruction.value
|
61
|
-
)
|
62
|
-
if maybe_item_or_collection_or_nil.nil?
|
63
|
-
false
|
64
|
-
elsif maybe_item_or_collection_or_nil.respond_to?(:each)
|
65
|
-
maybe_item_or_collection_or_nil.include? attribute_item_for(item)
|
66
|
-
else
|
67
|
-
maybe_item_or_collection_or_nil == attribute_item_for(item)
|
59
|
+
def filter_operation
|
60
|
+
@set.select do |item|
|
61
|
+
attribute_value_for(item)
|
62
|
+
.public_send(
|
63
|
+
operator,
|
64
|
+
attribute_value
|
65
|
+
)
|
68
66
|
end
|
69
67
|
end
|
70
|
-
# rubocop:enable Metrics/MethodLength
|
71
68
|
|
72
|
-
def
|
73
|
-
|
74
|
-
|
69
|
+
def intersect_operation
|
70
|
+
other_set = attribute_class
|
71
|
+
.public_send(
|
72
|
+
attribute,
|
73
|
+
attribute_value
|
74
|
+
)
|
75
|
+
@set & other_set
|
75
76
|
end
|
76
77
|
end
|
77
78
|
end
|
@@ -1,22 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative '../active_record_set_instruction'
|
4
|
+
|
3
5
|
class ActiveSet
|
4
6
|
module Sorting
|
5
7
|
class ActiveRecordStrategy
|
6
8
|
def initialize(set, attribute_instructions)
|
7
9
|
@set = set
|
8
10
|
@attribute_instructions = attribute_instructions
|
11
|
+
@set_instructions = attribute_instructions.map do |attribute_instruction|
|
12
|
+
ActiveRecordSetInstruction.new(attribute_instruction, set)
|
13
|
+
end
|
9
14
|
end
|
10
15
|
|
11
16
|
def execute
|
12
17
|
return false unless @set.respond_to? :to_sql
|
13
18
|
|
14
|
-
executable_instructions.reduce(
|
15
|
-
statement = set.merge(
|
16
|
-
|
17
|
-
return false if throws?(ActiveRecord::StatementInvalid) { statement.load }
|
19
|
+
executable_instructions.reduce(@set) do |set, set_instruction|
|
20
|
+
statement = set.merge(set_instruction.initial_relation)
|
21
|
+
statement = statement.merge(order_operation_for(set_instruction))
|
18
22
|
|
19
|
-
|
23
|
+
set_instruction.processed = true
|
20
24
|
statement
|
21
25
|
end
|
22
26
|
end
|
@@ -24,11 +28,11 @@ class ActiveSet
|
|
24
28
|
def executable_instructions
|
25
29
|
return {} unless @set.respond_to? :to_sql
|
26
30
|
|
27
|
-
@
|
28
|
-
attribute_model =
|
31
|
+
@set_instructions.select do |set_instruction|
|
32
|
+
attribute_model = set_instruction.attribute_model
|
29
33
|
next false unless attribute_model
|
30
34
|
next false unless attribute_model.respond_to?(:attribute_names)
|
31
|
-
next false unless attribute_model.attribute_names.include?(
|
35
|
+
next false unless attribute_model.attribute_names.include?(set_instruction.attribute)
|
32
36
|
|
33
37
|
true
|
34
38
|
end
|
@@ -36,40 +40,20 @@ class ActiveSet
|
|
36
40
|
|
37
41
|
private
|
38
42
|
|
39
|
-
def set_with_eager_loaded_associations
|
40
|
-
associations_hash = @attribute_instructions.reduce({}) { |h, i| h.merge(i.associations_hash) }
|
41
|
-
@set.eager_load(associations_hash)
|
42
|
-
end
|
43
|
-
|
44
43
|
# https://stackoverflow.com/a/44912964/2884386
|
45
44
|
# Force null values to be sorted as if larger than any non-null value
|
46
45
|
# ASC => [-2, -1, 1, 2, nil]
|
47
46
|
# DESC => [nil, 2, 1, -1, -2]
|
48
|
-
def order_operation_for(
|
49
|
-
attribute_model =
|
47
|
+
def order_operation_for(set_instruction)
|
48
|
+
attribute_model = set_instruction.attribute_model
|
50
49
|
|
51
|
-
arel_column =
|
52
|
-
|
53
|
-
arel_direction = direction_operator(attribute_instruction.value)
|
50
|
+
arel_column = set_instruction.arel_column
|
51
|
+
arel_direction = direction_operator(set_instruction.value)
|
54
52
|
nil_sorter = arel_column.send(arel_direction == :asc ? :eq : :not_eq, nil)
|
55
53
|
|
56
54
|
attribute_model.order(nil_sorter).order(arel_column.send(arel_direction))
|
57
55
|
end
|
58
56
|
|
59
|
-
def attribute_model_for(attribute_instruction)
|
60
|
-
return @set.klass if attribute_instruction.associations_array.empty?
|
61
|
-
|
62
|
-
attribute_instruction
|
63
|
-
.associations_array
|
64
|
-
.reduce(@set) do |obj, assoc|
|
65
|
-
obj.reflections[assoc.to_s]&.klass
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
def case_insensitive?(attribute_instruction)
|
70
|
-
attribute_instruction.operator.to_s.casecmp('i').zero?
|
71
|
-
end
|
72
|
-
|
73
57
|
def direction_operator(direction)
|
74
58
|
return :desc if direction.to_s.downcase.start_with? 'desc'
|
75
59
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative '../../helpers/transform_to_sortable_numeric'
|
4
|
+
require_relative '../enumerable_set_instruction'
|
4
5
|
|
5
6
|
class ActiveSet
|
6
7
|
module Sorting
|
@@ -8,14 +9,17 @@ class ActiveSet
|
|
8
9
|
def initialize(set, attribute_instructions)
|
9
10
|
@set = set
|
10
11
|
@attribute_instructions = attribute_instructions
|
12
|
+
@set_instructions = attribute_instructions.map do |attribute_instruction|
|
13
|
+
EnumerableSetInstruction.new(attribute_instruction, set)
|
14
|
+
end
|
11
15
|
end
|
12
16
|
|
13
17
|
def execute
|
14
18
|
# http://brandon.dimcheff.com/2009/11/18/rubys-sort-vs-sort-by/
|
15
19
|
@set.sort_by do |item|
|
16
|
-
@
|
17
|
-
value_for_comparison = sortable_numeric_for(
|
18
|
-
direction_multiplier = direction_multiplier(
|
20
|
+
@set_instructions.map do |set_instruction|
|
21
|
+
value_for_comparison = sortable_numeric_for(set_instruction, item)
|
22
|
+
direction_multiplier = direction_multiplier(set_instruction.value)
|
19
23
|
|
20
24
|
# Force null values to be sorted as if larger than any non-null value
|
21
25
|
# ASC => [-2, -1, 1, 2, nil]
|
@@ -29,23 +33,12 @@ class ActiveSet
|
|
29
33
|
end
|
30
34
|
end
|
31
35
|
|
32
|
-
def sortable_numeric_for(
|
33
|
-
value =
|
34
|
-
if value.is_a?(String) || value.is_a?(Symbol)
|
35
|
-
value = if case_insensitive?(instruction, value)
|
36
|
-
value.to_s.downcase
|
37
|
-
else
|
38
|
-
value.to_s
|
39
|
-
end
|
40
|
-
end
|
36
|
+
def sortable_numeric_for(set_instruction, item)
|
37
|
+
value = set_instruction.attribute_value_for(item)
|
41
38
|
|
42
39
|
transform_to_sortable_numeric(value)
|
43
40
|
end
|
44
41
|
|
45
|
-
def case_insensitive?(instruction, _value)
|
46
|
-
instruction.operator.to_s.casecmp('i').zero?
|
47
|
-
end
|
48
|
-
|
49
42
|
def direction_multiplier(direction)
|
50
43
|
return -1 if direction.to_s.downcase.start_with? 'desc'
|
51
44
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: actionset
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.
|
4
|
+
version: 0.8.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stephen Margheim
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-05-
|
11
|
+
date: 2019-05-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -280,8 +280,10 @@ files:
|
|
280
280
|
- lib/action_set/helpers/sort/next_direction_for_helper.rb
|
281
281
|
- lib/action_set/helpers/sort/path_for_helper.rb
|
282
282
|
- lib/active_set.rb
|
283
|
+
- lib/active_set/active_record_set_instruction.rb
|
283
284
|
- lib/active_set/attribute_instruction.rb
|
284
285
|
- lib/active_set/column_instruction.rb
|
286
|
+
- lib/active_set/enumerable_set_instruction.rb
|
285
287
|
- lib/active_set/exporting/csv_strategy.rb
|
286
288
|
- lib/active_set/exporting/operation.rb
|
287
289
|
- lib/active_set/filtering/active_record_strategy.rb
|