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
@@ -0,0 +1,308 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../constants'
4
+
5
+ class ActiveSet
6
+ module Filtering
7
+ module Enumerable
8
+ # rubocop:disable Metrics/ModuleLength
9
+ module Operators
10
+ NOT_TRANSFORMER = ->(result) { !result }
11
+ RANGE_TRANSFORMER = ->(value) { Range.new(*value.sort) }
12
+ REGEXP_TRANSFORMER = ->(value) { /#{Regexp.quote(value.to_s)}/ }
13
+ STRING_TRANSFORMER = ->(value) { value.to_s }
14
+
15
+ PREDICATES = {
16
+ EQ: {
17
+ operator: :'=='
18
+ },
19
+ NOT_EQ: {
20
+ operator: :'!='
21
+ },
22
+ EQ_ANY: {
23
+ operator: :'==',
24
+ reducer: :any?
25
+ },
26
+ EQ_ALL: {
27
+ operator: :'==',
28
+ reducer: :all?
29
+ },
30
+ NOT_EQ_ANY: {
31
+ operator: :'!=',
32
+ reducer: :any?
33
+ },
34
+ NOT_EQ_ALL: {
35
+ operator: :'!=',
36
+ reducer: :all?
37
+ },
38
+
39
+ IN: {
40
+ operator: :presence_in
41
+ },
42
+ NOT_IN: {
43
+ operator: :presence_in,
44
+ result_transformer: NOT_TRANSFORMER
45
+ },
46
+ IN_ANY: {
47
+ operator: :presence_in,
48
+ reducer: :any?
49
+ },
50
+ IN_ALL: {
51
+ operator: :presence_in,
52
+ reducer: :all?
53
+ },
54
+ NOT_IN_ANY: {
55
+ operator: :presence_in,
56
+ reducer: :any?,
57
+ result_transformer: NOT_TRANSFORMER
58
+ },
59
+ NOT_IN_ALL: {
60
+ operator: :presence_in,
61
+ reducer: :all?,
62
+ result_transformer: NOT_TRANSFORMER
63
+ },
64
+
65
+ MATCHES: {
66
+ operator: :'=~',
67
+ object_attribute_transformer: STRING_TRANSFORMER,
68
+ query_attribute_transformer: REGEXP_TRANSFORMER
69
+ },
70
+ DOES_NOT_MATCH: {
71
+ operator: :'!~',
72
+ object_attribute_transformer: STRING_TRANSFORMER,
73
+ query_attribute_transformer: REGEXP_TRANSFORMER
74
+ },
75
+ MATCHES_ANY: {
76
+ operator: :'=~',
77
+ reducer: :any?,
78
+ object_attribute_transformer: STRING_TRANSFORMER,
79
+ query_attribute_transformer: REGEXP_TRANSFORMER
80
+ },
81
+ MATCHES_ALL: {
82
+ operator: :'=~',
83
+ reducer: :all?,
84
+ object_attribute_transformer: STRING_TRANSFORMER,
85
+ query_attribute_transformer: REGEXP_TRANSFORMER
86
+ },
87
+ DOES_NOT_MATCH_ANY: {
88
+ operator: :'!~',
89
+ reducer: :any?,
90
+ object_attribute_transformer: STRING_TRANSFORMER,
91
+ query_attribute_transformer: REGEXP_TRANSFORMER
92
+ },
93
+ DOES_NOT_MATCH_ALL: {
94
+ operator: :'!~',
95
+ reducer: :all?,
96
+ object_attribute_transformer: STRING_TRANSFORMER,
97
+ query_attribute_transformer: REGEXP_TRANSFORMER
98
+ },
99
+
100
+ LT: {
101
+ operator: :'<'
102
+ },
103
+ LTEQ: {
104
+ operator: :'<='
105
+ },
106
+ LT_ANY: {
107
+ operator: :'<',
108
+ reducer: :any?
109
+ },
110
+ LT_ALL: {
111
+ operator: :'<',
112
+ reducer: :all?
113
+ },
114
+ LTEQ_ANY: {
115
+ operator: :'<=',
116
+ reducer: :any?
117
+ },
118
+ LTEQ_ALL: {
119
+ operator: :'<=',
120
+ reducer: :all?
121
+ },
122
+
123
+ GT: {
124
+ operator: :'>'
125
+ },
126
+ GTEQ: {
127
+ operator: :'>='
128
+ },
129
+ GT_ANY: {
130
+ operator: :'>',
131
+ reducer: :any?
132
+ },
133
+ GT_ALL: {
134
+ operator: :'>',
135
+ reducer: :all?
136
+ },
137
+ GTEQ_ANY: {
138
+ operator: :'>=',
139
+ reducer: :any?
140
+ },
141
+ GTEQ_ALL: {
142
+ operator: :'>=',
143
+ reducer: :all?
144
+ },
145
+
146
+ BETWEEN: {
147
+ operator: :cover?,
148
+ query_attribute_transformer: RANGE_TRANSFORMER
149
+ },
150
+ NOT_BETWEEN: {
151
+ operator: :cover?,
152
+ query_attribute_transformer: RANGE_TRANSFORMER,
153
+ result_transformer: NOT_TRANSFORMER
154
+ },
155
+
156
+ IS_TRUE: {
157
+ operator: :'==',
158
+ query_attribute_transformer: proc { |_| 1 }
159
+ },
160
+ IS_FALSE: {
161
+ operator: :'==',
162
+ query_attribute_transformer: proc { |_| 0 }
163
+ },
164
+
165
+ IS_NULL: {
166
+ operator: :'=='
167
+ },
168
+ NOT_NULL: {
169
+ operator: :'!='
170
+ },
171
+
172
+ IS_PRESENT: {
173
+ operator: :'!=',
174
+ reducer: :all?,
175
+ query_attribute_transformer: proc { |_| Constants::BLANK_VALUES }
176
+ },
177
+ IS_BLANK: {
178
+ operator: :'==',
179
+ reducer: :any?,
180
+ query_attribute_transformer: proc { |_| Constants::BLANK_VALUES }
181
+ },
182
+
183
+ MATCH_START: {
184
+ operator: :'start_with?',
185
+ object_attribute_transformer: STRING_TRANSFORMER,
186
+ query_attribute_transformer: STRING_TRANSFORMER
187
+ },
188
+ MATCH_START_ANY: {
189
+ operator: :'start_with?',
190
+ reducer: :any?,
191
+ object_attribute_transformer: STRING_TRANSFORMER,
192
+ query_attribute_transformer: STRING_TRANSFORMER
193
+ },
194
+ MATCH_START_ALL: {
195
+ operator: :'start_with?',
196
+ reducer: :all?,
197
+ object_attribute_transformer: STRING_TRANSFORMER,
198
+ query_attribute_transformer: STRING_TRANSFORMER
199
+ },
200
+ MATCH_NOT_START: {
201
+ operator: :'start_with?',
202
+ object_attribute_transformer: STRING_TRANSFORMER,
203
+ query_attribute_transformer: STRING_TRANSFORMER,
204
+ result_transformer: NOT_TRANSFORMER
205
+ },
206
+ MATCH_NOT_START_ANY: {
207
+ operator: :'start_with?',
208
+ reducer: :any?,
209
+ object_attribute_transformer: STRING_TRANSFORMER,
210
+ query_attribute_transformer: STRING_TRANSFORMER,
211
+ result_transformer: NOT_TRANSFORMER
212
+ },
213
+ MATCH_NOT_START_ALL: {
214
+ operator: :'start_with?',
215
+ reducer: :all?,
216
+ object_attribute_transformer: STRING_TRANSFORMER,
217
+ query_attribute_transformer: STRING_TRANSFORMER,
218
+ result_transformer: NOT_TRANSFORMER
219
+ },
220
+ MATCH_END: {
221
+ operator: :'end_with?',
222
+ object_attribute_transformer: STRING_TRANSFORMER,
223
+ query_attribute_transformer: STRING_TRANSFORMER
224
+ },
225
+ MATCH_END_ANY: {
226
+ operator: :'end_with?',
227
+ reducer: :any?,
228
+ object_attribute_transformer: STRING_TRANSFORMER,
229
+ query_attribute_transformer: STRING_TRANSFORMER
230
+ },
231
+ MATCH_END_ALL: {
232
+ operator: :'end_with?',
233
+ reducer: :all?,
234
+ object_attribute_transformer: STRING_TRANSFORMER,
235
+ query_attribute_transformer: STRING_TRANSFORMER
236
+ },
237
+ MATCH_NOT_END: {
238
+ operator: :'end_with?',
239
+ object_attribute_transformer: STRING_TRANSFORMER,
240
+ query_attribute_transformer: STRING_TRANSFORMER,
241
+ result_transformer: NOT_TRANSFORMER
242
+ },
243
+ MATCH_NOT_END_ANY: {
244
+ operator: :'end_with?',
245
+ reducer: :any?,
246
+ object_attribute_transformer: STRING_TRANSFORMER,
247
+ query_attribute_transformer: STRING_TRANSFORMER,
248
+ result_transformer: NOT_TRANSFORMER
249
+ },
250
+ MATCH_NOT_END_ALL: {
251
+ operator: :'end_with?',
252
+ reducer: :all?,
253
+ object_attribute_transformer: STRING_TRANSFORMER,
254
+ query_attribute_transformer: STRING_TRANSFORMER,
255
+ result_transformer: NOT_TRANSFORMER
256
+ },
257
+ MATCH_CONTAIN: {
258
+ operator: :'include?',
259
+ object_attribute_transformer: STRING_TRANSFORMER,
260
+ query_attribute_transformer: STRING_TRANSFORMER
261
+ },
262
+ MATCH_CONTAIN_ANY: {
263
+ operator: :'include?',
264
+ reducer: :any?,
265
+ object_attribute_transformer: STRING_TRANSFORMER,
266
+ query_attribute_transformer: STRING_TRANSFORMER
267
+ },
268
+ MATCH_CONTAIN_ALL: {
269
+ operator: :'include?',
270
+ reducer: :all?,
271
+ object_attribute_transformer: STRING_TRANSFORMER,
272
+ query_attribute_transformer: STRING_TRANSFORMER
273
+ },
274
+ MATCH_NOT_CONTAIN: {
275
+ operator: :'include?',
276
+ object_attribute_transformer: STRING_TRANSFORMER,
277
+ query_attribute_transformer: STRING_TRANSFORMER,
278
+ result_transformer: NOT_TRANSFORMER
279
+ },
280
+ MATCH_NOT_CONTAIN_ANY: {
281
+ operator: :'include?',
282
+ reducer: :any?,
283
+ object_attribute_transformer: STRING_TRANSFORMER,
284
+ query_attribute_transformer: STRING_TRANSFORMER,
285
+ result_transformer: NOT_TRANSFORMER
286
+ },
287
+ MATCH_NOT_CONTAIN_ALL: {
288
+ operator: :'include?',
289
+ reducer: :all?,
290
+ object_attribute_transformer: STRING_TRANSFORMER,
291
+ query_attribute_transformer: STRING_TRANSFORMER,
292
+ result_transformer: NOT_TRANSFORMER
293
+ }
294
+ }.freeze
295
+
296
+ def self.get(operator_name)
297
+ operator_key = operator_name.to_s.upcase.to_sym
298
+
299
+ base_operator_hash = Constants::PREDICATES.fetch(operator_key, {})
300
+ this_operator_hash = Operators::PREDICATES.fetch(operator_key, {})
301
+
302
+ base_operator_hash.merge(this_operator_hash)
303
+ end
304
+ end
305
+ # rubocop:enable Metrics/ModuleLength
306
+ end
307
+ end
308
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/array/wrap'
4
+ require_relative '../../enumerable_set_instruction'
5
+ require_relative './operators'
6
+
7
+ class ActiveSet
8
+ module Filtering
9
+ module Enumerable
10
+ class SetInstruction < EnumerableSetInstruction
11
+ def item_matches_query?(item)
12
+ return query_result_for(item, query_attribute_for(instruction_value)) unless operator_hash.key?(:reducer)
13
+
14
+ Array.wrap(instruction_value).public_send(operator_hash[:reducer]) do |value|
15
+ query_result_for(item, query_attribute_for(value))
16
+ end
17
+ end
18
+
19
+ def set_item
20
+ return @set_item if defined? @set_item
21
+
22
+ @set_item = @set.find(&:present?)
23
+ end
24
+
25
+ def attribute_instance
26
+ return set_item if @attribute_instruction.associations_array.empty?
27
+ return @attribute_model if defined? @attribute_model
28
+
29
+ @attribute_model = @attribute_instruction
30
+ .associations_array
31
+ .reduce(set_item) do |obj, assoc|
32
+ obj.public_send(assoc)
33
+ end
34
+ end
35
+
36
+ def attribute_class
37
+ attribute_instance&.class
38
+ end
39
+
40
+ private
41
+
42
+ def query_result_for(item, value)
43
+ result = if operator_method == :cover? && value.is_a?(Range)
44
+ value
45
+ .public_send(
46
+ operator_method,
47
+ object_attribute_for(item)
48
+ )
49
+ else
50
+ object_attribute_for(item)
51
+ .public_send(
52
+ operator_method,
53
+ value
54
+ )
55
+ end
56
+
57
+ return result unless operator_hash.key?(:result_transformer)
58
+
59
+ operator_hash[:result_transformer].call(result)
60
+ end
61
+
62
+ def object_attribute_for(item)
63
+ attribute = guarantee_attribute_type(attribute_value_for(item))
64
+ return attribute unless operator_hash.key?(:object_attribute_transformer)
65
+
66
+ operator_hash[:object_attribute_transformer].call(attribute)
67
+ end
68
+
69
+ def query_attribute_for(value)
70
+ attribute = guarantee_attribute_type(value)
71
+ return attribute unless operator_hash.key?(:query_attribute_transformer)
72
+
73
+ operator_hash[:query_attribute_transformer].call(attribute)
74
+ end
75
+
76
+ def operator_method
77
+ operator_hash.dig(:operator) || :'=='
78
+ end
79
+
80
+ def operator_hash
81
+ instruction_operator = @attribute_instruction.operator
82
+
83
+ Operators.get(instruction_operator)
84
+ end
85
+
86
+ def guarantee_attribute_type(attribute)
87
+ # Booleans don't respond to many operator methods,
88
+ # so we cast them to integers
89
+ return 1 if attribute == true
90
+ return 0 if attribute == false
91
+ return attribute.map { |a| guarantee_attribute_type(a) } if attribute.respond_to?(:each)
92
+
93
+ attribute
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,90 @@
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 Enumerable
9
+ class Strategy
10
+ delegate :attribute_instance,
11
+ :attribute_class,
12
+ :instruction_value,
13
+ :attribute_value_for,
14
+ :operator,
15
+ :attribute,
16
+ :set_item,
17
+ :resource_for,
18
+ to: :@set_instruction
19
+
20
+ def initialize(set, attribute_instruction)
21
+ @set = set
22
+ @attribute_instruction = attribute_instruction
23
+ @set_instruction = SetInstruction.new(attribute_instruction, set)
24
+ end
25
+
26
+ def execute
27
+ return false unless @set.respond_to? :select
28
+
29
+ if execute_filter_operation?
30
+ set = filter_operation
31
+ elsif execute_intersect_operation?
32
+ begin
33
+ set = intersect_operation
34
+ rescue TypeError # thrown if intersecting with a non-Array
35
+ return false
36
+ end
37
+ else
38
+ return false
39
+ end
40
+
41
+ set
42
+ end
43
+
44
+ private
45
+
46
+ def execute_filter_operation?
47
+ return false unless attribute_instance
48
+ return false unless attribute_instance.respond_to?(attribute)
49
+ return false if attribute_instance.method(attribute).arity.positive?
50
+
51
+ true
52
+ end
53
+
54
+ def execute_intersect_operation?
55
+ return false unless attribute_class
56
+ return false unless attribute_class.respond_to?(attribute)
57
+ return false if attribute_class.method(attribute).arity.zero?
58
+
59
+ true
60
+ end
61
+
62
+ def filter_operation
63
+ @set.select do |item|
64
+ @set_instruction.item_matches_query?(item)
65
+ end
66
+ end
67
+
68
+ def intersect_operation
69
+ @set & other_set
70
+ end
71
+
72
+ def other_set
73
+ other_set = attribute_class.public_send(
74
+ attribute,
75
+ instruction_value
76
+ )
77
+ if attribute_class != set_item.class
78
+ other_set = begin
79
+ @set.select { |item| resource_for(item: item)&.presence_in other_set }
80
+ rescue ArgumentError # thrown if other_set is doesn't respond to #include?, like when nil
81
+ nil
82
+ end
83
+ end
84
+
85
+ other_set
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end