actionset 0.8.1 → 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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -5
  3. data/.ruby-version +1 -1
  4. data/.travis.yml +1 -1
  5. data/CHANGELOG +67 -0
  6. data/Gemfile.lock +134 -126
  7. data/README.md +26 -0
  8. data/Rakefile +8 -1
  9. data/actionset.gemspec +2 -2
  10. data/lib/action_set.rb +8 -23
  11. data/lib/action_set/attribute_value.rb +7 -1
  12. data/lib/action_set/filter_instructions.rb +72 -0
  13. data/lib/action_set/helpers/helper_methods.rb +2 -1
  14. data/lib/action_set/helpers/pagination/record_description_for_helper.rb +20 -0
  15. data/lib/action_set/helpers/pagination/record_first_for_helper.rb +20 -0
  16. data/lib/action_set/helpers/pagination/record_last_for_helper.rb +26 -0
  17. data/lib/action_set/helpers/pagination/record_range_for_helper.rb +25 -0
  18. data/lib/action_set/helpers/pagination/record_size_for_helper.rb +9 -0
  19. data/lib/action_set/helpers/pagination/total_pages_for_helper.rb +3 -1
  20. data/lib/action_set/sort_instructions.rb +44 -0
  21. data/lib/active_set.rb +25 -2
  22. data/lib/active_set/active_record_set_instruction.rb +33 -32
  23. data/lib/active_set/attribute_instruction.rb +3 -3
  24. data/lib/active_set/enumerable_set_instruction.rb +13 -24
  25. data/lib/active_set/filtering/active_record/operators.rb +280 -0
  26. data/lib/active_set/filtering/active_record/query_column.rb +35 -0
  27. data/lib/active_set/filtering/active_record/query_value.rb +47 -0
  28. data/lib/active_set/filtering/active_record/set_instruction.rb +29 -0
  29. data/lib/active_set/filtering/active_record/strategy.rb +87 -0
  30. data/lib/active_set/filtering/constants.rb +349 -0
  31. data/lib/active_set/filtering/enumerable/operators.rb +308 -0
  32. data/lib/active_set/filtering/enumerable/set_instruction.rb +98 -0
  33. data/lib/active_set/filtering/enumerable/strategy.rb +90 -0
  34. data/lib/active_set/filtering/operation.rb +5 -8
  35. data/lib/active_set/paginating/active_record_strategy.rb +0 -2
  36. data/lib/active_set/sorting/active_record_strategy.rb +27 -3
  37. data/lib/active_set/sorting/enumerable_strategy.rb +12 -2
  38. data/lib/active_set/sorting/operation.rb +1 -2
  39. data/lib/helpers/flatten_keys_of.rb +53 -0
  40. data/lib/helpers/transform_to_sortable_numeric.rb +3 -3
  41. metadata +26 -13
  42. data/lib/active_set/filtering/active_record_strategy.rb +0 -85
  43. data/lib/active_set/filtering/enumerable_strategy.rb +0 -79
  44. data/lib/helpers/throws.rb +0 -19
  45. data/lib/patches/core_ext/hash/flatten_keys.rb +0 -58
@@ -9,15 +9,37 @@ class ActiveSet
9
9
  end
10
10
 
11
11
  def initial_relation
12
- return @set if @attribute_instruction.associations_array.empty?
12
+ return @initial_relation if defined? @initial_relation
13
13
 
14
- @set.eager_load(@attribute_instruction.associations_hash)
14
+ @initial_relation = if @attribute_instruction.associations_array.empty?
15
+ @set
16
+ else
17
+ @set.eager_load(@attribute_instruction.associations_hash)
18
+ end
15
19
  end
16
20
 
17
- def arel_type
18
- attribute_model
19
- .columns_hash[@attribute_instruction.attribute]
20
- .type
21
+ def arel_column
22
+ return @arel_column if defined? @arel_column
23
+
24
+ arel_column = arel_table[@attribute_instruction.attribute]
25
+ arel_column = arel_column.lower if case_insensitive_operation?
26
+
27
+ @arel_column = arel_column
28
+ end
29
+
30
+ def arel_column_name
31
+ arel_table[@attribute_instruction.attribute].name
32
+ end
33
+
34
+ def attribute_model
35
+ return @set.klass if @attribute_instruction.associations_array.empty?
36
+ return @attribute_model if defined? @attribute_model
37
+
38
+ @attribute_model = @attribute_instruction
39
+ .associations_array
40
+ .reduce(@set) do |obj, assoc|
41
+ obj.reflections[assoc.to_s]&.klass
42
+ end
21
43
  end
22
44
 
23
45
  def arel_table
@@ -31,37 +53,16 @@ class ActiveSet
31
53
  end
32
54
  end
33
55
 
34
- def arel_column
35
- _arel_column = arel_table[@attribute_instruction.attribute]
36
- return _arel_column.lower if case_insensitive_operation?
56
+ private
37
57
 
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
58
+ def arel_type
59
+ attribute_model
60
+ &.columns_hash[@attribute_instruction.attribute]
61
+ &.type
50
62
  end
51
63
 
52
64
  def case_insensitive_operation?
53
65
  @attribute_instruction.case_insensitive? && arel_type.presence_in(%i[string text])
54
66
  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
67
  end
67
68
  end
@@ -32,11 +32,11 @@ class ActiveSet
32
32
  @attribute = attribute
33
33
  end
34
34
 
35
- def operator(default: '==')
35
+ def operator
36
36
  return @operator if defined? @operator
37
37
 
38
38
  attribute_instruction = @keypath.last
39
- @operator = (attribute_instruction[operator_regex, 1] || default).to_sym
39
+ @operator = attribute_instruction[operator_regex, 1]&.to_sym
40
40
  end
41
41
 
42
42
  def options
@@ -92,7 +92,7 @@ class ActiveSet
92
92
  private
93
93
 
94
94
  def operator_regex
95
- %r{\((.*?)\)}
95
+ /\((.*?)\)/
96
96
  end
97
97
 
98
98
  def options_regex
@@ -9,16 +9,21 @@ class ActiveSet
9
9
  end
10
10
 
11
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
12
+ @item_values ||= Hash.new do |h, key|
13
+ item_value = @attribute_instruction.value_for(item: key)
14
+ item_value = item_value.downcase if case_insensitive_operation_for?(item_value)
15
+ h[key] = item_value
16
+ end
17
+
18
+ @item_values[item]
16
19
  end
17
20
 
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
21
+ def instruction_value
22
+ return @instruction_value if defined? @instruction_value
23
+
24
+ instruction_value = @attribute_instruction.value
25
+ instruction_value = instruction_value.downcase if case_insensitive_operation_for?(instruction_value)
26
+ @instruction_value = instruction_value
22
27
  end
23
28
 
24
29
  def case_insensitive_operation_for?(value)
@@ -26,21 +31,5 @@ class ActiveSet
26
31
 
27
32
  value.is_a?(String) || value.is_a?(Symbol)
28
33
  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
34
  end
46
35
  end
@@ -0,0 +1,280 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../constants'
4
+
5
+ class ActiveSet
6
+ module Filtering
7
+ module ActiveRecord
8
+ # rubocop:disable Metrics/ModuleLength
9
+ module Operators
10
+ RANGE_TRANSFORMER = proc do |raw:, sql:, type:|
11
+ if type.presence_in %i[boolean]
12
+ Range.new(*sql.sort)
13
+ else
14
+ Range.new(*raw.sort)
15
+ end
16
+ end
17
+ BLANK_TRANSFORMER = proc do |type:, **_ctx|
18
+ if type.presence_in %i[date float integer time datetime boolean]
19
+ [nil]
20
+ else
21
+ Constants::BLANK_VALUES
22
+ end
23
+ end
24
+ MATCHER_TRANSFORMER = proc do |sql:, type:, **ctx|
25
+ next sql.map { |str| MATCHER_TRANSFORMER.call(sql: str, type: type, **ctx) } if sql.respond_to?(:map)
26
+
27
+ next sql if type != :decimal
28
+ next sql[0..-3] if sql.ends_with?('.0')
29
+ next sql[0..-4] + '%' if sql.ends_with?('.0%')
30
+
31
+ sql
32
+ end
33
+ START_MATCHER_TRANSFORMER = proc do |sql:, type:, **ctx|
34
+ next sql.map { |str| START_MATCHER_TRANSFORMER.call(sql: str, type: type, **ctx) } if sql.respond_to?(:map)
35
+
36
+ str = MATCHER_TRANSFORMER.call(sql: sql, type: type, **ctx)
37
+ next str if ['boolean'].include? type.to_s
38
+
39
+ str + '%'
40
+ end
41
+ END_MATCHER_TRANSFORMER = proc do |sql:, type:, **ctx|
42
+ next sql.map { |str| END_MATCHER_TRANSFORMER.call(sql: str, type: type, **ctx) } if sql.respond_to?(:map)
43
+
44
+ str = MATCHER_TRANSFORMER.call(sql: sql, type: type, **ctx)
45
+ next str if ['boolean'].include? type.to_s
46
+
47
+ '%' + str
48
+ end
49
+ CONTAIN_MATCHER_TRANSFORMER = proc do |sql:, type:, **ctx|
50
+ next sql.map { |str| CONTAIN_MATCHER_TRANSFORMER.call(sql: str, type: type, **ctx) } if sql.respond_to?(:map)
51
+
52
+ str = MATCHER_TRANSFORMER.call(sql: sql, type: type, **ctx)
53
+ next str if ['boolean'].include? type.to_s
54
+
55
+ '%' + str + '%'
56
+ end
57
+
58
+ PREDICATES = {
59
+ EQ: {
60
+ operator: :eq
61
+ },
62
+ NOT_EQ: {
63
+ operator: :not_eq
64
+ },
65
+ EQ_ANY: {
66
+ operator: :eq_any
67
+ },
68
+ EQ_ALL: {
69
+ operator: :eq_all
70
+ },
71
+ NOT_EQ_ANY: {
72
+ operator: :not_eq_any
73
+ },
74
+ NOT_EQ_ALL: {
75
+ operator: :not_eq_all
76
+ },
77
+
78
+ IN: {
79
+ operator: :in
80
+ },
81
+ NOT_IN: {
82
+ operator: :not_in
83
+ },
84
+ IN_ANY: {
85
+ operator: :in_any
86
+ },
87
+ IN_ALL: {
88
+ operator: :in_all
89
+ },
90
+ NOT_IN_ANY: {
91
+ operator: :not_in_any
92
+ },
93
+ NOT_IN_ALL: {
94
+ operator: :not_in_all
95
+ },
96
+
97
+ MATCHES: {
98
+ operator: :matches,
99
+ query_attribute_transformer: MATCHER_TRANSFORMER
100
+ },
101
+ DOES_NOT_MATCH: {
102
+ operator: :does_not_match,
103
+ query_attribute_transformer: MATCHER_TRANSFORMER
104
+ },
105
+ MATCHES_ANY: {
106
+ operator: :matches_any,
107
+ query_attribute_transformer: MATCHER_TRANSFORMER
108
+ },
109
+ MATCHES_ALL: {
110
+ operator: :matches_all,
111
+ query_attribute_transformer: MATCHER_TRANSFORMER
112
+ },
113
+ DOES_NOT_MATCH_ANY: {
114
+ operator: :does_not_match_any,
115
+ query_attribute_transformer: MATCHER_TRANSFORMER
116
+ },
117
+ DOES_NOT_MATCH_ALL: {
118
+ operator: :does_not_match_all,
119
+ query_attribute_transformer: MATCHER_TRANSFORMER
120
+ },
121
+
122
+ LT: {
123
+ operator: :lt
124
+ },
125
+ LTEQ: {
126
+ operator: :lteq
127
+ },
128
+ LT_ANY: {
129
+ operator: :lt_any
130
+ },
131
+ LT_ALL: {
132
+ operator: :lt_all
133
+ },
134
+ LTEQ_ANY: {
135
+ operator: :lteq_any
136
+ },
137
+ LTEQ_ALL: {
138
+ operator: :lteq_all
139
+ },
140
+
141
+ GT: {
142
+ operator: :gt
143
+ },
144
+ GTEQ: {
145
+ operator: :gteq
146
+ },
147
+ GT_ANY: {
148
+ operator: :gt_any
149
+ },
150
+ GT_ALL: {
151
+ operator: :gt_all
152
+ },
153
+ GTEQ_ANY: {
154
+ operator: :gteq_any
155
+ },
156
+ GTEQ_ALL: {
157
+ operator: :gteq_all
158
+ },
159
+
160
+ BETWEEN: {
161
+ operator: :between,
162
+ query_attribute_transformer: RANGE_TRANSFORMER
163
+ },
164
+ NOT_BETWEEN: {
165
+ operator: :not_between,
166
+ query_attribute_transformer: RANGE_TRANSFORMER
167
+ },
168
+
169
+ IS_TRUE: {
170
+ operator: :eq,
171
+ query_attribute_transformer: proc { |_| true }
172
+ },
173
+ IS_FALSE: {
174
+ operator: :eq,
175
+ query_attribute_transformer: proc { |_| false }
176
+ },
177
+
178
+ IS_NULL: {
179
+ operator: :eq
180
+ },
181
+ NOT_NULL: {
182
+ operator: :not_eq
183
+ },
184
+
185
+ IS_PRESENT: {
186
+ operator: :not_eq_all,
187
+ query_attribute_transformer: BLANK_TRANSFORMER
188
+ },
189
+ IS_BLANK: {
190
+ operator: :eq_any,
191
+ query_attribute_transformer: BLANK_TRANSFORMER
192
+ },
193
+
194
+ MATCH_START: {
195
+ operator: :matches,
196
+ query_attribute_transformer: START_MATCHER_TRANSFORMER
197
+ },
198
+ MATCH_START_ANY: {
199
+ operator: :matches_any,
200
+ query_attribute_transformer: START_MATCHER_TRANSFORMER
201
+ },
202
+ MATCH_START_ALL: {
203
+ operator: :matches_all,
204
+ query_attribute_transformer: START_MATCHER_TRANSFORMER
205
+ },
206
+ MATCH_NOT_START: {
207
+ operator: :does_not_match,
208
+ query_attribute_transformer: START_MATCHER_TRANSFORMER
209
+ },
210
+ MATCH_NOT_START_ANY: {
211
+ operator: :does_not_match_any,
212
+ query_attribute_transformer: START_MATCHER_TRANSFORMER
213
+ },
214
+ MATCH_NOT_START_ALL: {
215
+ operator: :does_not_match_all,
216
+ query_attribute_transformer: START_MATCHER_TRANSFORMER
217
+ },
218
+ MATCH_END: {
219
+ operator: :matches,
220
+ query_attribute_transformer: END_MATCHER_TRANSFORMER
221
+ },
222
+ MATCH_END_ANY: {
223
+ operator: :matches_any,
224
+ query_attribute_transformer: END_MATCHER_TRANSFORMER
225
+ },
226
+ MATCH_END_ALL: {
227
+ operator: :matches_all,
228
+ query_attribute_transformer: END_MATCHER_TRANSFORMER
229
+ },
230
+ MATCH_NOT_END: {
231
+ operator: :does_not_match,
232
+ query_attribute_transformer: END_MATCHER_TRANSFORMER
233
+ },
234
+ MATCH_NOT_END_ANY: {
235
+ operator: :does_not_match_any,
236
+ query_attribute_transformer: END_MATCHER_TRANSFORMER
237
+ },
238
+ MATCH_NOT_END_ALL: {
239
+ operator: :does_not_match_all,
240
+ query_attribute_transformer: END_MATCHER_TRANSFORMER
241
+ },
242
+ MATCH_CONTAIN: {
243
+ operator: :matches,
244
+ query_attribute_transformer: CONTAIN_MATCHER_TRANSFORMER
245
+ },
246
+ MATCH_CONTAIN_ANY: {
247
+ operator: :matches_any,
248
+ query_attribute_transformer: CONTAIN_MATCHER_TRANSFORMER
249
+ },
250
+ MATCH_CONTAIN_ALL: {
251
+ operator: :matches_all,
252
+ query_attribute_transformer: CONTAIN_MATCHER_TRANSFORMER
253
+ },
254
+ MATCH_NOT_CONTAIN: {
255
+ operator: :does_not_match,
256
+ query_attribute_transformer: CONTAIN_MATCHER_TRANSFORMER
257
+ },
258
+ MATCH_NOT_CONTAIN_ANY: {
259
+ operator: :does_not_match_any,
260
+ query_attribute_transformer: CONTAIN_MATCHER_TRANSFORMER
261
+ },
262
+ MATCH_NOT_CONTAIN_ALL: {
263
+ operator: :does_not_match_all,
264
+ query_attribute_transformer: CONTAIN_MATCHER_TRANSFORMER
265
+ }
266
+ }.freeze
267
+
268
+ def self.get(operator_name)
269
+ operator_key = operator_name.to_s.upcase.to_sym
270
+
271
+ base_operator_hash = Constants::PREDICATES.fetch(operator_key, {})
272
+ this_operator_hash = Operators::PREDICATES.fetch(operator_key, {})
273
+
274
+ base_operator_hash.merge(this_operator_hash)
275
+ end
276
+ end
277
+ # rubocop:enable Metrics/ModuleLength
278
+ end
279
+ end
280
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveSet
4
+ module Filtering
5
+ module ActiveRecord
6
+ module QueryColumn
7
+ def query_column
8
+ return @query_column if defined? @query_column
9
+
10
+ @query_column = if must_cast_numerical_column?
11
+ column_cast_as_char
12
+ else
13
+ arel_column
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def column_cast_as_char
20
+ # In order to use LIKE, we must CAST the numeric column as a CHAR column.
21
+ # NOTE: this is can be quite inefficient, as it forces the DB engine to perform that cast on all rows.
22
+ # https://www.ryadel.com/en/like-operator-equivalent-integer-numeric-columns-sql-t-sql-database/
23
+ Arel::Nodes::NamedFunction.new('CAST', [arel_column.as('CHAR')])
24
+ end
25
+
26
+ def must_cast_numerical_column?
27
+ # The LIKE operator can't be used if the column hosts numeric types.
28
+ return false unless arel_type.presence_in(%i[integer float])
29
+
30
+ arel_operator.to_s.downcase.include?('match')
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end