actionset 0.8.1 → 0.11.0

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