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.
- checksums.yaml +4 -4
- data/CHANGELOG +24 -0
- data/Gemfile.lock +1 -1
- data/Rakefile +7 -1
- data/actionset.gemspec +1 -1
- data/lib/action_set.rb +36 -7
- data/lib/action_set/attribute_value.rb +7 -1
- data/lib/action_set/helpers/helper_methods.rb +2 -1
- data/lib/action_set/helpers/pagination/record_description_for_helper.rb +20 -0
- data/lib/action_set/helpers/pagination/record_first_for_helper.rb +20 -0
- data/lib/action_set/helpers/pagination/record_last_for_helper.rb +26 -0
- data/lib/action_set/helpers/pagination/record_range_for_helper.rb +25 -0
- data/lib/action_set/helpers/pagination/record_size_for_helper.rb +9 -0
- data/lib/action_set/helpers/pagination/total_pages_for_helper.rb +3 -1
- data/lib/active_set/active_record_set_instruction.rb +30 -37
- data/lib/active_set/attribute_instruction.rb +2 -2
- data/lib/active_set/enumerable_set_instruction.rb +13 -26
- data/lib/active_set/filtering/active_record/operators.rb +277 -0
- data/lib/active_set/filtering/active_record/query_column.rb +35 -0
- data/lib/active_set/filtering/active_record/query_value.rb +47 -0
- data/lib/active_set/filtering/active_record/set_instruction.rb +29 -0
- data/lib/active_set/filtering/active_record/strategy.rb +87 -0
- data/lib/active_set/filtering/constants.rb +349 -0
- data/lib/active_set/filtering/enumerable/operators.rb +308 -0
- data/lib/active_set/filtering/enumerable/set_instruction.rb +98 -0
- data/lib/active_set/filtering/enumerable/strategy.rb +90 -0
- data/lib/active_set/filtering/operation.rb +4 -4
- data/lib/helpers/flatten_keys_of.rb +1 -1
- metadata +16 -4
- data/lib/active_set/filtering/active_record_strategy.rb +0 -85
- 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
|