actionset 0.8.2 → 0.9.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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +24 -0
  3. data/Gemfile.lock +1 -1
  4. data/Rakefile +7 -1
  5. data/actionset.gemspec +1 -1
  6. data/lib/action_set.rb +36 -7
  7. data/lib/action_set/attribute_value.rb +7 -1
  8. data/lib/action_set/helpers/helper_methods.rb +2 -1
  9. data/lib/action_set/helpers/pagination/record_description_for_helper.rb +20 -0
  10. data/lib/action_set/helpers/pagination/record_first_for_helper.rb +20 -0
  11. data/lib/action_set/helpers/pagination/record_last_for_helper.rb +26 -0
  12. data/lib/action_set/helpers/pagination/record_range_for_helper.rb +25 -0
  13. data/lib/action_set/helpers/pagination/record_size_for_helper.rb +9 -0
  14. data/lib/action_set/helpers/pagination/total_pages_for_helper.rb +3 -1
  15. data/lib/active_set/active_record_set_instruction.rb +30 -37
  16. data/lib/active_set/attribute_instruction.rb +2 -2
  17. data/lib/active_set/enumerable_set_instruction.rb +13 -26
  18. data/lib/active_set/filtering/active_record/operators.rb +277 -0
  19. data/lib/active_set/filtering/active_record/query_column.rb +35 -0
  20. data/lib/active_set/filtering/active_record/query_value.rb +47 -0
  21. data/lib/active_set/filtering/active_record/set_instruction.rb +29 -0
  22. data/lib/active_set/filtering/active_record/strategy.rb +87 -0
  23. data/lib/active_set/filtering/constants.rb +349 -0
  24. data/lib/active_set/filtering/enumerable/operators.rb +308 -0
  25. data/lib/active_set/filtering/enumerable/set_instruction.rb +98 -0
  26. data/lib/active_set/filtering/enumerable/strategy.rb +90 -0
  27. data/lib/active_set/filtering/operation.rb +4 -4
  28. data/lib/helpers/flatten_keys_of.rb +1 -1
  29. metadata +16 -4
  30. data/lib/active_set/filtering/active_record_strategy.rb +0 -85
  31. data/lib/active_set/filtering/enumerable_strategy.rb +0 -79
@@ -0,0 +1,277 @@
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]
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
+
38
+ str + '%'
39
+ end
40
+ END_MATCHER_TRANSFORMER = proc do |sql:, type:, **ctx|
41
+ next sql.map { |str| END_MATCHER_TRANSFORMER.call(sql: str, type: type, **ctx) } if sql.respond_to?(:map)
42
+
43
+ str = MATCHER_TRANSFORMER.call(sql: sql, type: type, **ctx)
44
+
45
+ '%' + str
46
+ end
47
+ CONTAIN_MATCHER_TRANSFORMER = proc do |sql:, type:, **ctx|
48
+ next sql.map { |str| CONTAIN_MATCHER_TRANSFORMER.call(sql: str, type: type, **ctx) } if sql.respond_to?(:map)
49
+
50
+ str = MATCHER_TRANSFORMER.call(sql: sql, type: type, **ctx)
51
+
52
+ '%' + str + '%'
53
+ end
54
+
55
+ PREDICATES = {
56
+ EQ: {
57
+ operator: :eq
58
+ },
59
+ NOT_EQ: {
60
+ operator: :not_eq
61
+ },
62
+ EQ_ANY: {
63
+ operator: :eq_any
64
+ },
65
+ EQ_ALL: {
66
+ operator: :eq_all
67
+ },
68
+ NOT_EQ_ANY: {
69
+ operator: :not_eq_any
70
+ },
71
+ NOT_EQ_ALL: {
72
+ operator: :not_eq_all
73
+ },
74
+
75
+ IN: {
76
+ operator: :in
77
+ },
78
+ NOT_IN: {
79
+ operator: :not_in
80
+ },
81
+ IN_ANY: {
82
+ operator: :in_any
83
+ },
84
+ IN_ALL: {
85
+ operator: :in_all
86
+ },
87
+ NOT_IN_ANY: {
88
+ operator: :not_in_any
89
+ },
90
+ NOT_IN_ALL: {
91
+ operator: :not_in_all
92
+ },
93
+
94
+ MATCHES: {
95
+ operator: :matches,
96
+ query_attribute_transformer: MATCHER_TRANSFORMER
97
+ },
98
+ DOES_NOT_MATCH: {
99
+ operator: :does_not_match,
100
+ query_attribute_transformer: MATCHER_TRANSFORMER
101
+ },
102
+ MATCHES_ANY: {
103
+ operator: :matches_any,
104
+ query_attribute_transformer: MATCHER_TRANSFORMER
105
+ },
106
+ MATCHES_ALL: {
107
+ operator: :matches_all,
108
+ query_attribute_transformer: MATCHER_TRANSFORMER
109
+ },
110
+ DOES_NOT_MATCH_ANY: {
111
+ operator: :does_not_match_any,
112
+ query_attribute_transformer: MATCHER_TRANSFORMER
113
+ },
114
+ DOES_NOT_MATCH_ALL: {
115
+ operator: :does_not_match_all,
116
+ query_attribute_transformer: MATCHER_TRANSFORMER
117
+ },
118
+
119
+ LT: {
120
+ operator: :lt
121
+ },
122
+ LTEQ: {
123
+ operator: :lteq
124
+ },
125
+ LT_ANY: {
126
+ operator: :lt_any
127
+ },
128
+ LT_ALL: {
129
+ operator: :lt_all
130
+ },
131
+ LTEQ_ANY: {
132
+ operator: :lteq_any
133
+ },
134
+ LTEQ_ALL: {
135
+ operator: :lteq_all
136
+ },
137
+
138
+ GT: {
139
+ operator: :gt
140
+ },
141
+ GTEQ: {
142
+ operator: :gteq
143
+ },
144
+ GT_ANY: {
145
+ operator: :gt_any
146
+ },
147
+ GT_ALL: {
148
+ operator: :gt_all
149
+ },
150
+ GTEQ_ANY: {
151
+ operator: :gteq_any
152
+ },
153
+ GTEQ_ALL: {
154
+ operator: :gteq_all
155
+ },
156
+
157
+ BETWEEN: {
158
+ operator: :between,
159
+ query_attribute_transformer: RANGE_TRANSFORMER
160
+ },
161
+ NOT_BETWEEN: {
162
+ operator: :not_between,
163
+ query_attribute_transformer: RANGE_TRANSFORMER
164
+ },
165
+
166
+ IS_TRUE: {
167
+ operator: :eq,
168
+ query_attribute_transformer: proc { |_| true }
169
+ },
170
+ IS_FALSE: {
171
+ operator: :eq,
172
+ query_attribute_transformer: proc { |_| false }
173
+ },
174
+
175
+ IS_NULL: {
176
+ operator: :eq
177
+ },
178
+ NOT_NULL: {
179
+ operator: :not_eq
180
+ },
181
+
182
+ IS_PRESENT: {
183
+ operator: :not_eq_all,
184
+ query_attribute_transformer: BLANK_TRANSFORMER
185
+ },
186
+ IS_BLANK: {
187
+ operator: :eq_any,
188
+ query_attribute_transformer: BLANK_TRANSFORMER
189
+ },
190
+
191
+ MATCH_START: {
192
+ operator: :matches,
193
+ query_attribute_transformer: START_MATCHER_TRANSFORMER
194
+ },
195
+ MATCH_START_ANY: {
196
+ operator: :matches_any,
197
+ query_attribute_transformer: START_MATCHER_TRANSFORMER
198
+ },
199
+ MATCH_START_ALL: {
200
+ operator: :matches_all,
201
+ query_attribute_transformer: START_MATCHER_TRANSFORMER
202
+ },
203
+ MATCH_NOT_START: {
204
+ operator: :does_not_match,
205
+ query_attribute_transformer: START_MATCHER_TRANSFORMER
206
+ },
207
+ MATCH_NOT_START_ANY: {
208
+ operator: :does_not_match_any,
209
+ query_attribute_transformer: START_MATCHER_TRANSFORMER
210
+ },
211
+ MATCH_NOT_START_ALL: {
212
+ operator: :does_not_match_all,
213
+ query_attribute_transformer: START_MATCHER_TRANSFORMER
214
+ },
215
+ MATCH_END: {
216
+ operator: :matches,
217
+ query_attribute_transformer: END_MATCHER_TRANSFORMER
218
+ },
219
+ MATCH_END_ANY: {
220
+ operator: :matches_any,
221
+ query_attribute_transformer: END_MATCHER_TRANSFORMER
222
+ },
223
+ MATCH_END_ALL: {
224
+ operator: :matches_all,
225
+ query_attribute_transformer: END_MATCHER_TRANSFORMER
226
+ },
227
+ MATCH_NOT_END: {
228
+ operator: :does_not_match,
229
+ query_attribute_transformer: END_MATCHER_TRANSFORMER
230
+ },
231
+ MATCH_NOT_END_ANY: {
232
+ operator: :does_not_match_any,
233
+ query_attribute_transformer: END_MATCHER_TRANSFORMER
234
+ },
235
+ MATCH_NOT_END_ALL: {
236
+ operator: :does_not_match_all,
237
+ query_attribute_transformer: END_MATCHER_TRANSFORMER
238
+ },
239
+ MATCH_CONTAIN: {
240
+ operator: :matches,
241
+ query_attribute_transformer: CONTAIN_MATCHER_TRANSFORMER
242
+ },
243
+ MATCH_CONTAIN_ANY: {
244
+ operator: :matches_any,
245
+ query_attribute_transformer: CONTAIN_MATCHER_TRANSFORMER
246
+ },
247
+ MATCH_CONTAIN_ALL: {
248
+ operator: :matches_all,
249
+ query_attribute_transformer: CONTAIN_MATCHER_TRANSFORMER
250
+ },
251
+ MATCH_NOT_CONTAIN: {
252
+ operator: :does_not_match,
253
+ query_attribute_transformer: CONTAIN_MATCHER_TRANSFORMER
254
+ },
255
+ MATCH_NOT_CONTAIN_ANY: {
256
+ operator: :does_not_match_any,
257
+ query_attribute_transformer: CONTAIN_MATCHER_TRANSFORMER
258
+ },
259
+ MATCH_NOT_CONTAIN_ALL: {
260
+ operator: :does_not_match_all,
261
+ query_attribute_transformer: CONTAIN_MATCHER_TRANSFORMER
262
+ }
263
+ }.freeze
264
+
265
+ def self.get(operator_name)
266
+ operator_key = operator_name.to_s.upcase.to_sym
267
+
268
+ base_operator_hash = Constants::PREDICATES.fetch(operator_key, {})
269
+ this_operator_hash = Operators::PREDICATES.fetch(operator_key, {})
270
+
271
+ base_operator_hash.merge(this_operator_hash)
272
+ end
273
+ end
274
+ # rubocop:enable Metrics/ModuleLength
275
+ end
276
+ end
277
+ 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
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveSet
4
+ module Filtering
5
+ module ActiveRecord
6
+ module QueryValue
7
+ def query_value
8
+ return @query_value if defined? @query_value
9
+
10
+ query_value = @attribute_instruction.value
11
+ query_value = query_attribute_for(query_value)
12
+ query_value = query_value.downcase if case_insensitive_operation?
13
+
14
+ @query_value = query_value
15
+ end
16
+
17
+ private
18
+
19
+ def query_attribute_for(value)
20
+ return value unless operator_hash.key?(:query_attribute_transformer)
21
+
22
+ context = {
23
+ raw: value,
24
+ sql: to_sql_str(value),
25
+ type: arel_type
26
+ }
27
+
28
+ operator_hash[:query_attribute_transformer].call(context)
29
+ end
30
+
31
+ def to_sql_str(value)
32
+ return value.map { |a| to_sql_str(a) } if value.respond_to?(:map)
33
+
34
+ arel_node = Arel::Nodes::Casted.new(value, arel_column)
35
+ sql_value = arel_node.to_sql
36
+ unwrap_sql_str(sql_value)
37
+ end
38
+
39
+ def unwrap_sql_str(sql_str)
40
+ return sql_str unless sql_str[0] == "'" && sql_str[-1] == "'"
41
+
42
+ sql_str[1..-2]
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../active_record_set_instruction'
4
+ require_relative './operators'
5
+ require_relative './query_value'
6
+ require_relative './query_column'
7
+
8
+ class ActiveSet
9
+ module Filtering
10
+ module ActiveRecord
11
+ class SetInstruction < ActiveRecordSetInstruction
12
+ include QueryValue
13
+ include QueryColumn
14
+
15
+ def arel_operator
16
+ operator_hash.fetch(:operator, :eq)
17
+ end
18
+
19
+ private
20
+
21
+ def operator_hash
22
+ instruction_operator = @attribute_instruction.operator
23
+
24
+ Operators.get(instruction_operator)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './set_instruction'
4
+ require 'active_support/core_ext/module/delegation'
5
+
6
+ class ActiveSet
7
+ module Filtering
8
+ module ActiveRecord
9
+ class Strategy
10
+ delegate :attribute_model,
11
+ :query_column,
12
+ :arel_operator,
13
+ :query_value,
14
+ :arel_type,
15
+ :initial_relation,
16
+ :attribute,
17
+ to: :@set_instruction
18
+
19
+ def initialize(set, attribute_instruction)
20
+ @set = set
21
+ @attribute_instruction = attribute_instruction
22
+ @set_instruction = SetInstruction.new(attribute_instruction, set)
23
+ end
24
+
25
+ def execute
26
+ return false unless @set.respond_to? :to_sql
27
+
28
+ if execute_filter_operation?
29
+ statement = filter_operation
30
+ elsif execute_intersect_operation?
31
+ begin
32
+ statement = intersect_operation
33
+ rescue ArgumentError # thrown if merging a non-ActiveRecord::Relation
34
+ return false
35
+ end
36
+ else
37
+ return false
38
+ end
39
+
40
+ statement
41
+ end
42
+
43
+ private
44
+
45
+ def execute_filter_operation?
46
+ return false unless attribute_model
47
+ return false unless attribute_model.respond_to?(:attribute_names)
48
+ return false unless attribute_model.attribute_names.include?(attribute)
49
+
50
+ true
51
+ end
52
+
53
+ def execute_intersect_operation?
54
+ return false unless attribute_model
55
+ return false unless attribute_model.respond_to?(attribute)
56
+ return false if attribute_model.method(attribute).arity.zero?
57
+
58
+ true
59
+ end
60
+
61
+ def filter_operation
62
+ initial_relation
63
+ .where(
64
+ query_column.send(
65
+ arel_operator,
66
+ query_value
67
+ )
68
+ )
69
+ end
70
+
71
+ def intersect_operation
72
+ # NOTE: If merging relations that contain duplicate column conditions,
73
+ # the second condition will replace the first.
74
+ # e.g. Thing.where(id: [1,2]).merge(Thing.where(id: [2,3]))
75
+ # => [Thing<2>, Thing<3>] NOT [Thing<2>]
76
+ initial_relation
77
+ .merge(
78
+ attribute_model.public_send(
79
+ attribute,
80
+ query_value
81
+ )
82
+ )
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end