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
@@ -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