fetcheable_on_api 0.4.1 → 0.6.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/ASSOCIATION_SORTING_SOLUTION.md +119 -0
- data/CLAUDE.md +97 -0
- data/Gemfile.lock +54 -39
- data/README.md +311 -1
- data/lib/fetcheable_on_api/configuration.rb +33 -2
- data/lib/fetcheable_on_api/filterable.rb +274 -75
- data/lib/fetcheable_on_api/pageable.rb +85 -27
- data/lib/fetcheable_on_api/sortable.rb +192 -31
- data/lib/fetcheable_on_api/version.rb +5 -1
- data/lib/fetcheable_on_api.rb +160 -42
- data/lib/generators/fetcheable_on_api/install_generator.rb +15 -1
- data/lib/generators/templates/fetcheable_on_api_initializer.rb +14 -0
- metadata +9 -7
@@ -1,12 +1,43 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module FetcheableOnApi
|
4
|
-
# FetcheableOnApi
|
4
|
+
# Configuration class for FetcheableOnApi gem settings.
|
5
5
|
#
|
6
|
+
# This class holds global configuration options that affect the behavior
|
7
|
+
# of filtering, sorting, and pagination across all controllers that use
|
8
|
+
# the FetcheableOnApi module.
|
9
|
+
#
|
10
|
+
# Configuration is typically set in a Rails initializer file, but can
|
11
|
+
# also be modified at runtime if needed.
|
12
|
+
#
|
13
|
+
# @example Setting configuration in an initializer
|
14
|
+
# # config/initializers/fetcheable_on_api.rb
|
15
|
+
# FetcheableOnApi.configure do |config|
|
16
|
+
# config.pagination_default_size = 50
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# @example Runtime configuration changes
|
20
|
+
# FetcheableOnApi.configuration.pagination_default_size = 100
|
21
|
+
#
|
22
|
+
# @since 0.1.0
|
6
23
|
class Configuration
|
7
|
-
#
|
24
|
+
# Default number of records per page when no page[size] parameter is provided.
|
25
|
+
# This value is used by the Pageable module when clients don't specify
|
26
|
+
# a page size in their requests.
|
27
|
+
#
|
28
|
+
# @return [Integer] The default pagination size
|
29
|
+
# @example
|
30
|
+
# # With default configuration (25):
|
31
|
+
# # GET /users?page[number]=2
|
32
|
+
# # Returns 25 records starting from record 26
|
33
|
+
#
|
34
|
+
# # With custom configuration (50):
|
35
|
+
# # GET /users?page[number]=2
|
36
|
+
# # Returns 50 records starting from record 51
|
8
37
|
attr_accessor :pagination_default_size
|
9
38
|
|
39
|
+
# Initialize configuration with default values.
|
40
|
+
# Sets up sensible defaults that work well for most applications.
|
10
41
|
def initialize
|
11
42
|
@pagination_default_size = 25
|
12
43
|
end
|
@@ -1,11 +1,66 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module FetcheableOnApi
|
4
|
-
# Filterable implements `filter`
|
4
|
+
# Filterable implements support for JSONAPI-compliant filtering via `filter` query parameters.
|
5
|
+
#
|
6
|
+
# This module enables controllers to process filter parameters in the format:
|
7
|
+
# `filter[attribute]=value` or `filter[attribute]=value1,value2` for multiple values
|
8
|
+
#
|
9
|
+
# It supports:
|
10
|
+
# - 30+ Arel predicates (eq, ilike, between, in, gt, lt, matches, etc.)
|
11
|
+
# - Association filtering with custom class names
|
12
|
+
# - Custom lambda predicates for complex filtering logic
|
13
|
+
# - Multiple filter values with OR logic
|
14
|
+
# - Date/time filtering with custom formats
|
15
|
+
#
|
16
|
+
# @example Basic filtering setup
|
17
|
+
# class UsersController < ApplicationController
|
18
|
+
# filter_by :name, :email, :status
|
19
|
+
#
|
20
|
+
# def index
|
21
|
+
# users = apply_fetcheable(User.all)
|
22
|
+
# render json: users
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# # GET /users?filter[name]=john&filter[status]=active
|
27
|
+
#
|
28
|
+
# @example Association filtering
|
29
|
+
# class PostsController < ApplicationController
|
30
|
+
# filter_by :title
|
31
|
+
# filter_by :author, class_name: User, as: 'name'
|
32
|
+
#
|
33
|
+
# def index
|
34
|
+
# posts = apply_fetcheable(Post.joins(:author))
|
35
|
+
# render json: posts
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# # GET /posts?filter[author]=john&filter[title]=rails
|
40
|
+
#
|
41
|
+
# @example Custom predicates
|
42
|
+
# class ProductsController < ApplicationController
|
43
|
+
# filter_by :price, with: :gteq # Greater than or equal
|
44
|
+
# filter_by :created_at, with: :between, format: :datetime
|
45
|
+
#
|
46
|
+
# def index
|
47
|
+
# products = apply_fetcheable(Product.all)
|
48
|
+
# render json: products
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# # GET /products?filter[price]=100&filter[created_at]=1609459200,1640995200
|
53
|
+
#
|
54
|
+
# @see https://jsonapi.org/format/#fetching-filtering JSONAPI Filtering Specification
|
5
55
|
module Filterable
|
56
|
+
# Arel predicates that expect array values instead of single values.
|
57
|
+
# These predicates work with multiple values and are handled differently
|
58
|
+
# during parameter validation and processing.
|
6
59
|
#
|
7
|
-
#
|
8
|
-
#
|
60
|
+
# @example Usage with array predicates
|
61
|
+
# filter_by :tags, with: :in_all
|
62
|
+
# # Expects: filter[tags][]= or filter[tags]=value1,value2
|
63
|
+
# Arel predicates that expect array values instead of single values.
|
9
64
|
PREDICATES_WITH_ARRAY = %i[
|
10
65
|
does_not_match_all
|
11
66
|
does_not_match_any
|
@@ -29,87 +84,148 @@ module FetcheableOnApi
|
|
29
84
|
not_in_any
|
30
85
|
].freeze
|
31
86
|
|
87
|
+
# Hook called when Filterable is included in a class.
|
88
|
+
# Sets up the class to support filter configuration and provides
|
89
|
+
# the filter_by class method.
|
32
90
|
#
|
33
|
-
#
|
34
|
-
#
|
91
|
+
# @param base [Class] The class including this module
|
92
|
+
# @private
|
35
93
|
def self.included(base)
|
36
94
|
base.class_eval do
|
37
95
|
extend ClassMethods
|
96
|
+
# Store filter configurations per class to avoid conflicts between controllers
|
38
97
|
class_attribute :filters_configuration, instance_writer: false
|
39
98
|
self.filters_configuration = {}
|
40
99
|
end
|
41
100
|
end
|
42
101
|
|
43
|
-
# Class methods made available to
|
102
|
+
# Class methods made available to controllers when Filterable is included.
|
44
103
|
module ClassMethods
|
45
|
-
# Define
|
104
|
+
# Define one or more filterable attributes for the controller.
|
105
|
+
#
|
106
|
+
# This method configures which model attributes can be filtered via query parameters
|
107
|
+
# and how those filters should be processed.
|
108
|
+
#
|
109
|
+
# @param attrs [Array<Symbol>] List of attribute names to make filterable
|
110
|
+
# @param options [Hash] Configuration options for the filters
|
111
|
+
# @option options [String, Symbol] :as Alias for the database column name
|
112
|
+
# @option options [Class] :class_name Model class for association filtering (defaults to collection class)
|
113
|
+
# @option options [Symbol, Proc] :with Arel predicate to use (:ilike, :eq, :between, etc.) or custom lambda
|
114
|
+
# @option options [Symbol] :format Value format (:string, :array, :datetime) for parameter processing
|
115
|
+
# @option options [Symbol] :association Association name when different from inferred name
|
46
116
|
#
|
47
|
-
# @
|
117
|
+
# @example Basic attribute filtering
|
118
|
+
# filter_by :name, :email, :status
|
119
|
+
# # Allows: filter[name]=john&filter[email]=john@example.com&filter[status]=active
|
48
120
|
#
|
49
|
-
# @
|
50
|
-
#
|
51
|
-
#
|
52
|
-
#
|
121
|
+
# @example Custom predicate
|
122
|
+
# filter_by :age, with: :gteq # Greater than or equal
|
123
|
+
# filter_by :created_at, with: :between, format: :datetime
|
124
|
+
# # Allows: filter[age]=18&filter[created_at]=1609459200,1640995200
|
125
|
+
#
|
126
|
+
# @example Association filtering
|
127
|
+
# filter_by :author, class_name: User, as: 'name'
|
128
|
+
# # Allows: filter[author]=john (filters by users.name)
|
129
|
+
#
|
130
|
+
# @example Custom lambda predicate
|
131
|
+
# filter_by :full_name, with: -> (collection, value) {
|
132
|
+
# collection.arel_table[:first_name].matches("%#{value}%").or(
|
133
|
+
# collection.arel_table[:last_name].matches("%#{value}%")
|
134
|
+
# )
|
135
|
+
# }
|
136
|
+
#
|
137
|
+
# @raise [ArgumentError] When invalid options are provided
|
138
|
+
# @see PREDICATES_WITH_ARRAY For list of array-based predicates
|
53
139
|
def filter_by(*attrs)
|
54
140
|
options = attrs.extract_options!
|
55
141
|
options.symbolize_keys!
|
56
|
-
options.assert_valid_keys(
|
57
|
-
:as, :class_name, :with, :format, :association
|
58
|
-
)
|
59
142
|
|
143
|
+
# Validate that only supported options are provided
|
144
|
+
options.assert_valid_keys(:as, :class_name, :with, :format, :association)
|
145
|
+
|
146
|
+
# Create a new configuration hash to avoid modifying parent class config
|
60
147
|
self.filters_configuration = filters_configuration.dup
|
61
148
|
|
62
149
|
attrs.each do |attr|
|
150
|
+
# Initialize default configuration for this attribute
|
63
151
|
filters_configuration[attr] ||= {
|
64
|
-
as: options[:as] || attr
|
152
|
+
as: options[:as] || attr
|
65
153
|
}
|
66
154
|
|
155
|
+
# Merge in the provided options
|
67
156
|
filters_configuration[attr].merge!(options)
|
68
157
|
end
|
69
158
|
end
|
70
159
|
end
|
71
160
|
|
72
|
-
#
|
73
|
-
# Public instance methods
|
74
|
-
#
|
161
|
+
# Protected instance methods for filtering functionality
|
75
162
|
|
76
|
-
#
|
77
|
-
# Protected instance methods
|
78
|
-
#
|
79
163
|
protected
|
80
164
|
|
165
|
+
# Generate the list of valid parameter keys for Rails strong parameters.
|
166
|
+
# This method examines the filter configuration to determine which parameters
|
167
|
+
# should be permitted, taking into account predicates that expect arrays.
|
168
|
+
#
|
169
|
+
# @return [Array] Array of parameter keys, with Hash format for array predicates
|
170
|
+
# @example
|
171
|
+
# # For filter_by :name, :tags (where tags uses in_any predicate)
|
172
|
+
# # Returns: [:name, {tags: []}]
|
173
|
+
# @private
|
81
174
|
def valid_keys
|
82
175
|
keys = filters_configuration.keys
|
83
176
|
keys.each_with_index do |key, index|
|
84
177
|
predicate = filters_configuration[key.to_sym].fetch(:with, :ilike)
|
85
178
|
|
86
|
-
|
179
|
+
# Special handling for predicates that work with ranges or arrays
|
180
|
+
if %i[between not_between in in_all in_any].include?(predicate)
|
87
181
|
format = filters_configuration[key.to_sym].fetch(:format) { nil }
|
88
|
-
|
182
|
+
# Use array format for explicit array formatting or for array predicates
|
183
|
+
if format == :array || PREDICATES_WITH_ARRAY.include?(predicate.to_sym)
|
184
|
+
keys[index] = { key => [] }
|
185
|
+
end
|
89
186
|
next
|
90
187
|
end
|
91
188
|
|
189
|
+
# Skip if it's a custom lambda predicate or doesn't expect arrays
|
92
190
|
next if predicate.respond_to?(:call) ||
|
93
191
|
PREDICATES_WITH_ARRAY.exclude?(predicate.to_sym)
|
94
192
|
|
95
|
-
|
193
|
+
# Convert to array format for predicates that expect multiple values
|
194
|
+
keys[index] = { key => [] }
|
96
195
|
end
|
97
196
|
|
98
197
|
keys
|
99
198
|
end
|
100
199
|
|
200
|
+
# Apply filtering to the collection based on filter query parameters.
|
201
|
+
# This is the main method that processes all configured filters and
|
202
|
+
# applies them to the ActiveRecord relation.
|
203
|
+
#
|
204
|
+
# @param collection [ActiveRecord::Relation] The collection to filter
|
205
|
+
# @return [ActiveRecord::Relation] The filtered collection
|
206
|
+
# @raise [FetcheableOnApi::ArgumentError] When filter parameters are invalid
|
207
|
+
#
|
208
|
+
# @example
|
209
|
+
# # With params: { filter: { name: 'john', status: 'active' } }
|
210
|
+
# filtered_users = apply_filters(User.all)
|
211
|
+
# # Generates: WHERE users.name ILIKE '%john%' AND users.status ILIKE '%active%'
|
101
212
|
def apply_filters(collection)
|
213
|
+
# Return early if no filter parameters are provided
|
102
214
|
return collection if params[:filter].blank?
|
103
215
|
|
216
|
+
# Validate that filter parameters are properly formatted
|
104
217
|
foa_valid_parameters!(:filter)
|
105
218
|
|
219
|
+
# Extract and permit only configured filter parameters
|
106
220
|
filter_params = params.require(:filter)
|
107
|
-
.permit(valid_keys)
|
221
|
+
.permit(*valid_keys)
|
108
222
|
.to_hash
|
109
223
|
|
224
|
+
# Process each filter parameter and build Arel predicates
|
110
225
|
filtering = filter_params.map do |column, values|
|
111
226
|
config = filters_configuration[column.to_sym]
|
112
227
|
|
228
|
+
# Extract configuration for this filter
|
113
229
|
format = config.fetch(:format, :string)
|
114
230
|
column_name = config.fetch(:as, column)
|
115
231
|
klass = config.fetch(:class_name, collection.klass)
|
@@ -120,66 +236,133 @@ module FetcheableOnApi
|
|
120
236
|
|
121
237
|
predicate = config.fetch(:with, :ilike)
|
122
238
|
|
239
|
+
# Join association table if filtering on a different model
|
123
240
|
if collection_klass != klass
|
124
241
|
collection = collection.joins(association_class_or_name)
|
125
242
|
end
|
126
243
|
|
244
|
+
# Skip if values is nil or empty
|
245
|
+
next if values.nil? || values == ""
|
246
|
+
|
247
|
+
# Handle range-based predicates (between, not_between)
|
127
248
|
if %i[between not_between].include?(predicate)
|
128
249
|
if values.is_a?(String)
|
129
|
-
|
250
|
+
# Single range: "start,end"
|
251
|
+
range_values = values.split(',')
|
252
|
+
range_values = apply_format_conversion(range_values, format)
|
253
|
+
predicates(predicate, collection, klass, column_name, range_values)
|
130
254
|
else
|
255
|
+
# Multiple ranges: ["start1,end1", "start2,end2"] with OR logic
|
131
256
|
values.map do |value|
|
132
|
-
|
257
|
+
range_values = value.split(',')
|
258
|
+
range_values = apply_format_conversion(range_values, format)
|
259
|
+
predicates(predicate, collection, klass, column_name, range_values)
|
133
260
|
end.inject(:or)
|
134
261
|
end
|
135
262
|
elsif values.is_a?(String)
|
136
|
-
values
|
263
|
+
# Single value or comma-separated values with OR logic
|
264
|
+
split_values = values.split(',')
|
265
|
+
split_values = apply_format_conversion(split_values, format)
|
266
|
+
split_values.map do |value|
|
137
267
|
predicates(predicate, collection, klass, column_name, value)
|
138
268
|
end.inject(:or)
|
139
269
|
else
|
140
|
-
|
141
|
-
|
270
|
+
# Array of values, each potentially comma-separated
|
271
|
+
flat_values = values.map { |el| el.split(',') }.flatten
|
272
|
+
converted_values = apply_format_conversion(flat_values, format)
|
273
|
+
predicates(predicate, collection, klass, column_name, converted_values)
|
142
274
|
end
|
143
275
|
end
|
144
276
|
|
277
|
+
# Combine all filter predicates with AND logic
|
145
278
|
collection.where(filtering.flatten.compact.inject(:and))
|
146
279
|
end
|
147
280
|
|
148
|
-
#
|
281
|
+
# Build an Arel predicate for the given parameters.
|
282
|
+
# This method translates filter predicates into Arel expressions that can
|
283
|
+
# be used in ActiveRecord where clauses.
|
284
|
+
#
|
285
|
+
# @param predicate [Symbol, Proc] The predicate type (:eq, :ilike, :between, etc.) or custom lambda
|
286
|
+
# @param collection [ActiveRecord::Relation] The collection being filtered (used for lambda predicates)
|
287
|
+
# @param klass [Class] The model class for the attribute being filtered
|
288
|
+
# @param column_name [String, Symbol] The database column name to filter on
|
289
|
+
# @param value [Object] The filter value(s) to compare against
|
290
|
+
# @return [Arel::Node] An Arel predicate node
|
291
|
+
# @raise [ArgumentError] When an unsupported predicate is used
|
292
|
+
#
|
293
|
+
# @example
|
294
|
+
# # predicates(:eq, collection, User, 'name', 'john')
|
295
|
+
# # Returns: users.name = 'john'
|
296
|
+
#
|
297
|
+
# # predicates(:between, collection, User, 'age', [18, 65])
|
298
|
+
# # Returns: users.age BETWEEN 18 AND 65
|
299
|
+
#
|
300
|
+
# @private
|
149
301
|
def predicates(predicate, collection, klass, column_name, value)
|
150
302
|
case predicate
|
303
|
+
# Range predicates - work with two values (start, end)
|
151
304
|
when :between
|
152
305
|
klass.arel_table[column_name].between(value.first..value.last)
|
153
|
-
when :
|
154
|
-
klass.arel_table[column_name].
|
155
|
-
|
156
|
-
|
157
|
-
when :does_not_match_any
|
158
|
-
klass.arel_table[column_name].does_not_match_any(value)
|
306
|
+
when :not_between
|
307
|
+
klass.arel_table[column_name].not_between(value.first..value.last)
|
308
|
+
|
309
|
+
# Equality predicates - exact matching
|
159
310
|
when :eq
|
160
311
|
klass.arel_table[column_name].eq(value)
|
312
|
+
when :not_eq
|
313
|
+
klass.arel_table[column_name].not_eq(value)
|
314
|
+
|
315
|
+
# Comparison predicates - numeric/date comparisons
|
316
|
+
when :gt
|
317
|
+
klass.arel_table[column_name].gt(value)
|
318
|
+
when :gteq
|
319
|
+
klass.arel_table[column_name].gteq(value)
|
320
|
+
when :lt
|
321
|
+
klass.arel_table[column_name].lt(value)
|
322
|
+
when :lteq
|
323
|
+
klass.arel_table[column_name].lteq(value)
|
324
|
+
|
325
|
+
# Array inclusion predicates - check if value is in a set
|
326
|
+
when :in
|
327
|
+
if value.is_a?(Array)
|
328
|
+
klass.arel_table[column_name].in(value.flatten.compact.uniq)
|
329
|
+
else
|
330
|
+
klass.arel_table[column_name].in(value)
|
331
|
+
end
|
332
|
+
when :not_in
|
333
|
+
klass.arel_table[column_name].not_in(value)
|
334
|
+
|
335
|
+
# Pattern matching predicates - for text search
|
336
|
+
when :ilike
|
337
|
+
# Default predicate - case-insensitive partial matching
|
338
|
+
klass.arel_table[column_name].matches("%#{value}%")
|
339
|
+
when :matches
|
340
|
+
# Exact pattern matching (supports SQL wildcards)
|
341
|
+
klass.arel_table[column_name].matches(value)
|
342
|
+
when :does_not_match
|
343
|
+
klass.arel_table[column_name].does_not_match("%#{value}%")
|
344
|
+
|
345
|
+
# Array-based predicates (work with multiple values)
|
161
346
|
when :eq_all
|
162
347
|
klass.arel_table[column_name].eq_all(value)
|
163
348
|
when :eq_any
|
164
349
|
klass.arel_table[column_name].eq_any(value)
|
165
|
-
when :gt
|
166
|
-
klass.arel_table[column_name].gt(value)
|
167
350
|
when :gt_all
|
168
351
|
klass.arel_table[column_name].gt_all(value)
|
169
352
|
when :gt_any
|
170
353
|
klass.arel_table[column_name].gt_any(value)
|
171
|
-
when :gteq
|
172
|
-
klass.arel_table[column_name].gteq(value)
|
173
354
|
when :gteq_all
|
174
355
|
klass.arel_table[column_name].gteq_all(value)
|
175
356
|
when :gteq_any
|
176
357
|
klass.arel_table[column_name].gteq_any(value)
|
177
|
-
when :
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
358
|
+
when :lt_all
|
359
|
+
klass.arel_table[column_name].lt_all(value)
|
360
|
+
when :lt_any
|
361
|
+
klass.arel_table[column_name].lt_any(value)
|
362
|
+
when :lteq_all
|
363
|
+
klass.arel_table[column_name].lteq_all(value)
|
364
|
+
when :lteq_any
|
365
|
+
klass.arel_table[column_name].lteq_any(value)
|
183
366
|
when :in_all
|
184
367
|
if value.is_a?(Array)
|
185
368
|
klass.arel_table[column_name].in_all(value.flatten.compact.uniq)
|
@@ -192,51 +375,67 @@ module FetcheableOnApi
|
|
192
375
|
else
|
193
376
|
klass.arel_table[column_name].in_any(value)
|
194
377
|
end
|
195
|
-
when :lt
|
196
|
-
klass.arel_table[column_name].lt(value)
|
197
|
-
when :lt_all
|
198
|
-
klass.arel_table[column_name].lt_all(value)
|
199
|
-
when :lt_any
|
200
|
-
klass.arel_table[column_name].lt_any(value)
|
201
|
-
when :lteq
|
202
|
-
klass.arel_table[column_name].lteq(value)
|
203
|
-
when :lteq_all
|
204
|
-
klass.arel_table[column_name].lteq_all(value)
|
205
|
-
when :lteq_any
|
206
|
-
klass.arel_table[column_name].lteq_any(value)
|
207
|
-
when :ilike
|
208
|
-
klass.arel_table[column_name].matches("%#{value}%")
|
209
|
-
when :matches
|
210
|
-
klass.arel_table[column_name].matches(value)
|
211
|
-
when :matches_all
|
212
|
-
klass.arel_table[column_name].matches_all(value)
|
213
|
-
when :matches_any
|
214
|
-
klass.arel_table[column_name].matches_any(value)
|
215
|
-
when :not_between
|
216
|
-
klass.arel_table[column_name].not_between(value.first..value.last)
|
217
|
-
when :not_eq
|
218
|
-
klass.arel_table[column_name].not_eq(value)
|
219
378
|
when :not_eq_all
|
220
379
|
klass.arel_table[column_name].not_eq_all(value)
|
221
380
|
when :not_eq_any
|
222
381
|
klass.arel_table[column_name].not_eq_any(value)
|
223
|
-
when :not_in
|
224
|
-
klass.arel_table[column_name].not_in(value)
|
225
382
|
when :not_in_all
|
226
383
|
klass.arel_table[column_name].not_in_all(value)
|
227
384
|
when :not_in_any
|
228
385
|
klass.arel_table[column_name].not_in_any(value)
|
386
|
+
when :matches_all
|
387
|
+
klass.arel_table[column_name].matches_all(value)
|
388
|
+
when :matches_any
|
389
|
+
klass.arel_table[column_name].matches_any(value)
|
390
|
+
when :does_not_match_all
|
391
|
+
klass.arel_table[column_name].does_not_match_all(value)
|
392
|
+
when :does_not_match_any
|
393
|
+
klass.arel_table[column_name].does_not_match_any(value)
|
229
394
|
else
|
395
|
+
# Handle custom lambda predicates
|
230
396
|
unless predicate.respond_to?(:call)
|
231
397
|
raise ArgumentError,
|
232
398
|
"unsupported predicate `#{predicate}`"
|
233
399
|
end
|
234
400
|
|
401
|
+
# Call the custom predicate with collection and value
|
235
402
|
predicate.call(collection, value)
|
236
403
|
end
|
237
404
|
end
|
238
405
|
|
239
|
-
#
|
406
|
+
# Apply format conversion to values based on the specified format.
|
407
|
+
# This method handles value transformation for different data types,
|
408
|
+
# particularly datetime conversion using the foa_string_to_datetime method.
|
409
|
+
#
|
410
|
+
# @param values [Object] The values to convert (String, Array, etc.)
|
411
|
+
# @param format [Symbol] The format to apply (:string, :array, :datetime)
|
412
|
+
# @return [Object] The converted values
|
413
|
+
# @private
|
414
|
+
def apply_format_conversion(values, format)
|
415
|
+
case format
|
416
|
+
when :datetime
|
417
|
+
if values.is_a?(Array)
|
418
|
+
values.map { |value| foa_string_to_datetime(value.to_s) }
|
419
|
+
elsif values.is_a?(String)
|
420
|
+
foa_string_to_datetime(values)
|
421
|
+
else
|
422
|
+
values
|
423
|
+
end
|
424
|
+
when :array
|
425
|
+
# Array format is handled in parameter parsing, not value conversion
|
426
|
+
values
|
427
|
+
else
|
428
|
+
# :string or any other format - no conversion needed
|
429
|
+
values
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
# Override the default permitted types to allow Arrays for filter parameters.
|
434
|
+
# Filtering supports more flexible parameter types compared to sorting/pagination
|
435
|
+
# since filter values can be arrays of values for certain predicates.
|
436
|
+
#
|
437
|
+
# @return [Array<Class>] Array of permitted parameter types for filtering
|
438
|
+
# @private
|
240
439
|
def foa_default_permitted_types
|
241
440
|
[ActionController::Parameters, Hash, Array]
|
242
441
|
end
|