fetcheable_on_api 0.4 → 0.5.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.
- checksums.yaml +4 -4
- data/ASSOCIATION_SORTING_SOLUTION.md +119 -0
- data/CLAUDE.md +97 -0
- data/Gemfile.lock +54 -39
- data/README.md +315 -1
- data/lib/fetcheable_on_api/configuration.rb +33 -2
- data/lib/fetcheable_on_api/filterable.rb +250 -77
- 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,113 +1,274 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module FetcheableOnApi
|
4
|
-
# Sortable implements `
|
4
|
+
# Sortable implements support for JSONAPI-compliant sorting via `sort` query parameters.
|
5
|
+
#
|
6
|
+
# This module enables controllers to process sort parameters in the format:
|
7
|
+
# `sort=field1,-field2,+field3` where:
|
8
|
+
# - No prefix or `+` prefix means ascending order
|
9
|
+
# - `-` prefix means descending order
|
10
|
+
# - Multiple fields are comma-separated and applied in order
|
11
|
+
#
|
12
|
+
# It supports:
|
13
|
+
# - Single and multiple field sorting
|
14
|
+
# - Ascending and descending sort directions
|
15
|
+
# - Association sorting with custom class names
|
16
|
+
# - Case-insensitive sorting with the `lower` option
|
17
|
+
# - Field aliasing for different database column names
|
18
|
+
#
|
19
|
+
# @example Basic sorting setup
|
20
|
+
# class UsersController < ApplicationController
|
21
|
+
# sort_by :name, :email, :created_at
|
22
|
+
#
|
23
|
+
# def index
|
24
|
+
# users = apply_fetcheable(User.all)
|
25
|
+
# render json: users
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# # GET /users?sort=name,-created_at (name asc, created_at desc)
|
30
|
+
#
|
31
|
+
# @example Association sorting
|
32
|
+
# class PostsController < ApplicationController
|
33
|
+
# sort_by :title, :created_at
|
34
|
+
# sort_by :author, class_name: User, as: 'name'
|
35
|
+
#
|
36
|
+
# def index
|
37
|
+
# posts = apply_fetcheable(Post.joins(:author))
|
38
|
+
# render json: posts
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# # GET /posts?sort=author,-created_at (by author name asc, then created_at desc)
|
43
|
+
#
|
44
|
+
# @example Case-insensitive sorting
|
45
|
+
# class UsersController < ApplicationController
|
46
|
+
# sort_by :name, lower: true # Sort by lowercase name
|
47
|
+
# sort_by :email, :created_at
|
48
|
+
#
|
49
|
+
# def index
|
50
|
+
# users = apply_fetcheable(User.all)
|
51
|
+
# render json: users
|
52
|
+
# end
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# # GET /users?sort=name (sorts by LOWER(users.name))
|
56
|
+
#
|
57
|
+
# @see https://jsonapi.org/format/#fetching-sorting JSONAPI Sorting Specification
|
5
58
|
module Sortable
|
59
|
+
# Maps sort direction prefixes to Arel sort methods.
|
60
|
+
# Used to parse the sort parameter and determine ascending vs descending order.
|
6
61
|
#
|
7
|
-
#
|
8
|
-
#
|
62
|
+
# @example
|
63
|
+
# # "+name" or "name" -> :asc (ascending)
|
64
|
+
# # "-name" -> :desc (descending)
|
9
65
|
SORT_ORDER = {
|
10
|
-
|
11
|
-
|
66
|
+
'+' => :asc, # Explicit ascending (same as no prefix)
|
67
|
+
'-' => :desc, # Explicit descending
|
12
68
|
}.freeze
|
13
69
|
|
70
|
+
# Hook called when Sortable is included in a class.
|
71
|
+
# Sets up the class to support sort configuration and provides
|
72
|
+
# the sort_by class method.
|
14
73
|
#
|
15
|
-
#
|
16
|
-
#
|
74
|
+
# @param base [Class] The class including this module
|
75
|
+
# @private
|
17
76
|
def self.included(base)
|
18
77
|
base.class_eval do
|
19
78
|
extend ClassMethods
|
79
|
+
# Store sort configurations per class to avoid conflicts between controllers
|
20
80
|
class_attribute :sorts_configuration, instance_writer: false
|
21
81
|
self.sorts_configuration = {}
|
22
82
|
end
|
23
83
|
end
|
24
84
|
|
25
|
-
# Class methods made available to
|
85
|
+
# Class methods made available to controllers when Sortable is included.
|
26
86
|
module ClassMethods
|
27
|
-
# Define one
|
87
|
+
# Define one or more sortable attributes for the controller.
|
88
|
+
#
|
89
|
+
# This method configures which model attributes can be sorted via query parameters
|
90
|
+
# and how those sorts should be processed.
|
91
|
+
#
|
92
|
+
# @param attrs [Array<Symbol>] List of attribute names to make sortable
|
93
|
+
# @param options [Hash] Configuration options for the sorts
|
94
|
+
# @option options [String, Symbol] :as Alias for the database column name
|
95
|
+
# @option options [Boolean] :lower Whether to sort on the lowercase version of the attribute
|
96
|
+
# @option options [Class] :class_name Model class for association sorting (defaults to collection class)
|
97
|
+
# @option options [Symbol] :association Association name when different from inferred name
|
98
|
+
#
|
99
|
+
# @example Basic attribute sorting
|
100
|
+
# sort_by :name, :email, :created_at
|
101
|
+
# # Allows: sort=name,-email,created_at
|
102
|
+
#
|
103
|
+
# @example Case-insensitive sorting
|
104
|
+
# sort_by :name, lower: true
|
105
|
+
# # Generates: ORDER BY LOWER(users.name)
|
106
|
+
#
|
107
|
+
# @example Association sorting
|
108
|
+
# sort_by :author, class_name: User, as: 'name'
|
109
|
+
# # Allows: sort=author (sorts by users.name)
|
28
110
|
#
|
29
|
-
# @
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
111
|
+
# @example Association sorting with custom association name
|
112
|
+
# sort_by :author_name, class_name: User, as: 'name', association: :author
|
113
|
+
# # Allows: sort=author_name (sorts by users.name via author association)
|
114
|
+
# # Note: Make sure your collection is joined: Book.joins(:author)
|
115
|
+
#
|
116
|
+
# @example Field aliasing
|
117
|
+
# sort_by :full_name, as: 'name'
|
118
|
+
# # Maps sort=full_name to ORDER BY users.name
|
34
119
|
def sort_by(*attrs)
|
35
120
|
options = attrs.extract_options!
|
36
121
|
options.symbolize_keys!
|
37
122
|
|
123
|
+
# Validate that only supported options are provided
|
124
|
+
options.assert_valid_keys(:as, :class_name, :lower, :association)
|
125
|
+
|
126
|
+
# Create a new configuration hash to avoid modifying parent class config
|
38
127
|
self.sorts_configuration = sorts_configuration.dup
|
39
128
|
|
40
129
|
attrs.each do |attr|
|
130
|
+
# Initialize default configuration for this attribute
|
41
131
|
sorts_configuration[attr] ||= {
|
42
|
-
as: attr
|
132
|
+
as: attr
|
43
133
|
}
|
44
134
|
|
135
|
+
# Merge in the provided options, overriding defaults
|
45
136
|
sorts_configuration[attr] = sorts_configuration[attr].merge(options)
|
46
137
|
end
|
47
138
|
end
|
48
139
|
end
|
49
140
|
|
50
|
-
#
|
51
|
-
# Public instance methods
|
52
|
-
#
|
141
|
+
# Protected instance methods for sorting functionality
|
53
142
|
|
54
|
-
#
|
55
|
-
# Protected instance methods
|
56
|
-
#
|
57
143
|
protected
|
58
144
|
|
145
|
+
# Apply sorting to the collection based on sort query parameters.
|
146
|
+
# This is the main method that processes the sort parameter and
|
147
|
+
# applies ordering to the ActiveRecord relation.
|
148
|
+
#
|
149
|
+
# @param collection [ActiveRecord::Relation] The collection to sort
|
150
|
+
# @return [ActiveRecord::Relation] The sorted collection
|
151
|
+
# @raise [FetcheableOnApi::ArgumentError] When sort parameters are invalid
|
152
|
+
#
|
153
|
+
# @example
|
154
|
+
# # With params: { sort: 'name,-created_at' }
|
155
|
+
# sorted_users = apply_sort(User.all)
|
156
|
+
# # Generates: ORDER BY users.name ASC, users.created_at DESC
|
59
157
|
def apply_sort(collection)
|
158
|
+
# Return early if no sort parameters are provided
|
60
159
|
return collection if params[:sort].blank?
|
61
160
|
|
161
|
+
# Validate that sort parameter is a string
|
62
162
|
foa_valid_parameters!(:sort, foa_permitted_types: [String])
|
63
163
|
|
164
|
+
# Parse the sort parameter and build Arel ordering expressions
|
64
165
|
ordering = format_params(params[:sort]).map do |attr_name, sort_method|
|
65
166
|
arel_sort(attr_name, sort_method, collection)
|
66
167
|
end
|
67
168
|
|
169
|
+
# Apply the ordering, filtering out any nil values (unconfigured sorts)
|
68
170
|
collection.order(ordering.compact)
|
69
171
|
end
|
70
172
|
|
71
173
|
private
|
72
174
|
|
175
|
+
# Build an Arel ordering expression for the given attribute and sort direction.
|
176
|
+
# Returns nil if the attribute is not configured for sorting or doesn't exist on the model.
|
177
|
+
#
|
178
|
+
# @param attr_name [Symbol] The attribute name to sort by
|
179
|
+
# @param sort_method [Symbol] The sort direction (:asc or :desc)
|
180
|
+
# @param collection [ActiveRecord::Relation] The collection being sorted
|
181
|
+
# @return [Arel::Node, nil] An Arel ordering node or nil if invalid
|
182
|
+
# @private
|
73
183
|
def arel_sort(attr_name, sort_method, collection)
|
184
|
+
# Skip if this attribute is not configured for sorting
|
74
185
|
return if sorts_configuration[attr_name].blank?
|
75
186
|
|
76
187
|
klass = class_for(attr_name, collection)
|
77
188
|
field = field_for(attr_name)
|
189
|
+
|
190
|
+
# Skip if the field doesn't exist on the model
|
78
191
|
return unless belong_to_attributes_for?(klass, field)
|
79
192
|
|
193
|
+
# Build the Arel attribute reference using the appropriate table
|
80
194
|
attribute = klass.arel_table[field]
|
81
|
-
attribute = attribute.lower if sorts_configuration[attr_name].fetch(:lower, false)
|
82
195
|
|
196
|
+
# Apply lowercase transformation if configured
|
197
|
+
config = sorts_configuration[attr_name] || {}
|
198
|
+
attribute = attribute.lower if config.fetch(:lower, false)
|
199
|
+
|
200
|
+
# Apply the sort direction (asc or desc)
|
83
201
|
attribute.send(sort_method)
|
84
202
|
end
|
85
203
|
|
204
|
+
# Determine the model class to use for this sort attribute.
|
205
|
+
# Uses the configured class_name or falls back to the collection's class.
|
206
|
+
#
|
207
|
+
# @param attr_name [Symbol] The attribute name
|
208
|
+
# @param collection [ActiveRecord::Relation] The collection being sorted
|
209
|
+
# @return [Class] The model class to use
|
210
|
+
# @private
|
86
211
|
def class_for(attr_name, collection)
|
87
|
-
sorts_configuration[attr_name]
|
212
|
+
config = sorts_configuration[attr_name] || {}
|
213
|
+
config.fetch(:class_name, collection.klass)
|
88
214
|
end
|
89
215
|
|
216
|
+
# Get the database field name for this sort attribute.
|
217
|
+
# Uses the configured alias (:as option) or the attribute name itself.
|
218
|
+
#
|
219
|
+
# @param attr_name [Symbol] The attribute name
|
220
|
+
# @return [String] The database column name
|
221
|
+
# @private
|
90
222
|
def field_for(attr_name)
|
91
|
-
sorts_configuration[attr_name]
|
223
|
+
config = sorts_configuration[attr_name] || {}
|
224
|
+
config.fetch(:as, attr_name).to_s
|
92
225
|
end
|
93
226
|
|
227
|
+
# Check if the given field exists as an attribute on the model class.
|
228
|
+
# This prevents SQL errors from trying to sort by non-existent columns.
|
229
|
+
#
|
230
|
+
# @param klass [Class] The model class
|
231
|
+
# @param field [String] The field name to check
|
232
|
+
# @return [Boolean] True if the field exists on the model
|
233
|
+
# @private
|
94
234
|
def belong_to_attributes_for?(klass, field)
|
95
235
|
klass.attribute_names.include?(field)
|
96
236
|
end
|
97
237
|
|
238
|
+
# Parse the sort parameter string into a hash of attributes and directions.
|
98
239
|
#
|
99
|
-
#
|
100
|
-
#
|
240
|
+
# This method takes a comma-separated string of sort fields (with optional
|
241
|
+
# direction prefixes) and converts it into a hash mapping field names to
|
242
|
+
# sort directions.
|
101
243
|
#
|
244
|
+
# @param params [String] The sort parameter string
|
245
|
+
# @return [Hash{Symbol => Symbol}] Hash mapping attribute names to sort directions
|
246
|
+
#
|
247
|
+
# @example
|
248
|
+
# format_params("-email,first_name,+last_name")
|
249
|
+
# # => { email: :desc, first_name: :asc, last_name: :asc }
|
250
|
+
#
|
251
|
+
# format_params("name")
|
252
|
+
# # => { name: :asc }
|
253
|
+
#
|
254
|
+
# @private
|
102
255
|
def format_params(params)
|
103
|
-
|
256
|
+
result = {}
|
257
|
+
|
104
258
|
params
|
105
|
-
.split(
|
259
|
+
.split(',') # Split on commas to get individual fields
|
106
260
|
.each do |attribute|
|
107
|
-
|
108
|
-
|
261
|
+
# Trim whitespace
|
262
|
+
attribute = attribute.strip
|
263
|
+
|
264
|
+
# Extract the direction prefix (+ or -) or default to +
|
265
|
+
sort_sign = attribute =~ /\A[+-]/ ? attribute.slice!(0) : '+'
|
266
|
+
|
267
|
+
# Map the field name to its sort direction
|
268
|
+
result[attribute.to_sym] = SORT_ORDER[sort_sign]
|
109
269
|
end
|
110
|
-
|
270
|
+
|
271
|
+
result
|
111
272
|
end
|
112
273
|
end
|
113
274
|
end
|
@@ -1,5 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module FetcheableOnApi
|
4
|
-
|
4
|
+
# Current version of the FetcheableOnApi gem.
|
5
|
+
# Follows semantic versioning (semver.org) principles.
|
6
|
+
#
|
7
|
+
# @return [String] The version string in format "MAJOR.MINOR.PATCH"
|
8
|
+
VERSION = '0.5.0'.freeze
|
5
9
|
end
|
data/lib/fetcheable_on_api.rb
CHANGED
@@ -1,49 +1,96 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
3
|
+
require 'fetcheable_on_api/configuration'
|
4
|
+
require 'fetcheable_on_api/filterable'
|
5
|
+
require 'fetcheable_on_api/pageable'
|
6
|
+
require 'fetcheable_on_api/sortable'
|
7
|
+
require 'fetcheable_on_api/version'
|
8
|
+
require 'active_support'
|
9
|
+
require 'date'
|
10
10
|
|
11
11
|
# FetcheableOnApi provides standardized sorting, filtering and pagination for
|
12
|
-
#
|
12
|
+
# Rails API controllers following the JSONAPI specification.
|
13
13
|
#
|
14
|
+
# This gem automatically adds support for query parameters like:
|
15
|
+
# - `filter[attribute]=value` for filtering data
|
16
|
+
# - `sort=attribute1,-attribute2` for sorting (- prefix for descending)
|
17
|
+
# - `page[number]=1&page[size]=25` for pagination
|
18
|
+
#
|
19
|
+
# @example Basic usage in a controller
|
20
|
+
# class UsersController < ApplicationController
|
21
|
+
# # Configure allowed filters and sorts
|
22
|
+
# filter_by :name, :email, :status
|
23
|
+
# sort_by :name, :created_at, :updated_at
|
24
|
+
#
|
25
|
+
# def index
|
26
|
+
# users = apply_fetcheable(User.all)
|
27
|
+
# render json: users
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# @example Using with associations
|
32
|
+
# class PostsController < ApplicationController
|
33
|
+
# filter_by :title
|
34
|
+
# filter_by :author, class_name: User, as: 'name'
|
35
|
+
# sort_by :title, :created_at
|
36
|
+
# sort_by :author, class_name: User, as: 'name'
|
37
|
+
#
|
38
|
+
# def index
|
39
|
+
# posts = apply_fetcheable(Post.joins(:author).includes(:author))
|
40
|
+
# render json: posts
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# @author Fabien Piette
|
45
|
+
# @since 0.1.0
|
14
46
|
module FetcheableOnApi
|
15
|
-
#
|
16
|
-
#
|
47
|
+
# Global configuration settings for FetcheableOnApi.
|
48
|
+
# This method provides access to the singleton configuration instance
|
49
|
+
# that can be used to customize default behavior across the application.
|
17
50
|
#
|
18
51
|
# @example Set default pagination size
|
19
52
|
# FetcheableOnApi.configuration.pagination_default_size = 25
|
20
53
|
#
|
21
54
|
# @return [Configuration] The global configuration instance
|
55
|
+
# @see Configuration
|
22
56
|
def self.configuration
|
23
57
|
@configuration ||= Configuration.new
|
24
58
|
end
|
25
59
|
|
26
60
|
# Configure FetcheableOnApi using a block.
|
61
|
+
# This is the recommended way to set up configuration in an initializer.
|
27
62
|
#
|
28
|
-
# @example Set default pagination size
|
63
|
+
# @example Set default pagination size in config/initializers/fetcheable_on_api.rb
|
29
64
|
# FetcheableOnApi.configure do |config|
|
30
|
-
# config.pagination_default_size =
|
65
|
+
# config.pagination_default_size = 50
|
31
66
|
# end
|
32
67
|
#
|
33
|
-
# @yield [Configuration] Gives the global instance to the block
|
68
|
+
# @yield [Configuration] Gives the global configuration instance to the block
|
69
|
+
# @see Configuration
|
34
70
|
def self.configure
|
35
71
|
yield(configuration)
|
36
72
|
end
|
37
73
|
|
38
|
-
#
|
39
|
-
#
|
40
|
-
#
|
74
|
+
# Custom exception classes for FetcheableOnApi-specific errors.
|
75
|
+
# These inherit from standard Ruby exceptions but allow for more
|
76
|
+
# specific error handling in applications using this gem.
|
77
|
+
|
78
|
+
# Raised when invalid parameters are provided to filtering, sorting, or pagination
|
79
|
+
# @example
|
80
|
+
# raise FetcheableOnApi::ArgumentError, "Invalid filter parameter type"
|
41
81
|
ArgumentError = Class.new(ArgumentError)
|
82
|
+
|
83
|
+
# Raised when a feature is not yet implemented or supported
|
84
|
+
# @example
|
85
|
+
# raise FetcheableOnApi::NotImplementedError, "Custom predicate not supported"
|
42
86
|
NotImplementedError = Class.new(NotImplementedError)
|
43
87
|
|
88
|
+
# Hook called when this module is included in a class.
|
89
|
+
# Automatically includes the three main concern modules that provide
|
90
|
+
# filtering, sorting, and pagination functionality.
|
44
91
|
#
|
45
|
-
#
|
46
|
-
#
|
92
|
+
# @param klass [Class] The class that is including FetcheableOnApi
|
93
|
+
# @private
|
47
94
|
def self.included(klass)
|
48
95
|
klass.class_eval do
|
49
96
|
include Filterable
|
@@ -52,60 +99,131 @@ module FetcheableOnApi
|
|
52
99
|
end
|
53
100
|
end
|
54
101
|
|
102
|
+
# Protected instance methods available to controllers that include this module
|
103
|
+
|
104
|
+
protected
|
105
|
+
|
106
|
+
# Apply filters, sorting, and pagination to a collection in sequence.
|
107
|
+
# This is the main entry point for processing JSONAPI query parameters.
|
55
108
|
#
|
56
|
-
#
|
109
|
+
# The operations are applied in this specific order:
|
110
|
+
# 1. Filtering (apply_filters) - reduces the dataset
|
111
|
+
# 2. Sorting (apply_sort) - orders the results
|
112
|
+
# 3. Pagination (apply_pagination) - limits and offsets for page
|
57
113
|
#
|
58
|
-
|
114
|
+
# @param collection [ActiveRecord::Relation] The base collection to process
|
115
|
+
# @return [ActiveRecord::Relation] The processed collection with filters, sorting, and pagination applied
|
59
116
|
#
|
60
|
-
#
|
117
|
+
# @example Basic usage
|
118
|
+
# def index
|
119
|
+
# users = apply_fetcheable(User.all)
|
120
|
+
# render json: users
|
121
|
+
# end
|
61
122
|
#
|
62
|
-
|
63
|
-
|
64
|
-
#
|
123
|
+
# @example With joins for association filtering/sorting
|
124
|
+
# def index
|
125
|
+
# posts = apply_fetcheable(Post.joins(:author).includes(:author))
|
126
|
+
# render json: posts
|
127
|
+
# end
|
65
128
|
def apply_fetcheable(collection)
|
129
|
+
# Apply filtering first to reduce dataset size
|
66
130
|
collection = apply_filters(collection)
|
131
|
+
|
132
|
+
# Apply sorting to the filtered results
|
67
133
|
collection = apply_sort(collection)
|
68
134
|
|
135
|
+
# Apply pagination last to get the final page
|
69
136
|
apply_pagination(collection)
|
70
137
|
end
|
71
138
|
|
72
|
-
#
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
139
|
+
# Validates that the specified parameter keys contain values of permitted types.
|
140
|
+
# This is used internally by the filtering, sorting, and pagination modules
|
141
|
+
# to ensure that malformed or malicious parameters don't cause errors.
|
142
|
+
#
|
143
|
+
# @param keys [Array<Symbol>] Path to the parameter to validate (e.g., [:filter], [:page, :number])
|
144
|
+
# @param foa_permitted_types [Array<Class>] Array of allowed parameter types
|
145
|
+
# @raise [FetcheableOnApi::ArgumentError] When parameter type is not in permitted types
|
146
|
+
#
|
147
|
+
# @example
|
148
|
+
# # Validates that params[:filter] is a Hash or ActionController::Parameters
|
149
|
+
# foa_valid_parameters!(:filter)
|
150
|
+
#
|
151
|
+
# # Validates that params[:sort] is a String
|
152
|
+
# foa_valid_parameters!(:sort, foa_permitted_types: [String])
|
153
|
+
#
|
154
|
+
# @private
|
155
|
+
def foa_valid_parameters!(*keys, foa_permitted_types: foa_default_permitted_types)
|
156
|
+
return if foa_valid_params_types(*keys, foa_permitted_types: foa_permitted_types)
|
79
157
|
|
158
|
+
actual_type = params.dig(*keys).class
|
80
159
|
raise FetcheableOnApi::ArgumentError,
|
81
|
-
"Incorrect type #{
|
160
|
+
"Incorrect type #{actual_type} for params #{keys}"
|
82
161
|
end
|
83
162
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
163
|
+
# Checks if the parameter value at the specified keys matches any of the permitted types.
|
164
|
+
#
|
165
|
+
# @param keys [Array<Symbol>] Path to the parameter to check
|
166
|
+
# @param foa_permitted_types [Array<Class>] Array of allowed parameter types
|
167
|
+
# @return [Boolean] True if the parameter type is valid, false otherwise
|
168
|
+
# @private
|
169
|
+
def foa_valid_params_types(*keys, foa_permitted_types: foa_default_permitted_types)
|
170
|
+
foa_permitted_types.inject(false) do |result, type|
|
171
|
+
result || foa_valid_params_type(params.dig(*keys), type)
|
88
172
|
end
|
89
173
|
end
|
90
174
|
|
91
|
-
#
|
92
|
-
#
|
93
|
-
#
|
175
|
+
# Checks if a value is of the specified type using Ruby's is_a? method.
|
176
|
+
# This handles inheritance and module inclusion correctly.
|
177
|
+
#
|
178
|
+
# @param value [Object] The value to type-check
|
179
|
+
# @param type [Class] The expected type/class
|
180
|
+
# @return [Boolean] True if value is an instance of type (or its subclass/module)
|
181
|
+
# @private
|
94
182
|
def foa_valid_params_type(value, type)
|
95
183
|
value.is_a?(type)
|
96
184
|
end
|
97
185
|
|
98
|
-
#
|
186
|
+
# Default permitted parameter types for most operations.
|
187
|
+
# ActionController::Parameters is the standard Rails params object,
|
188
|
+
# while Hash is allowed for direct hash parameters in tests or non-Rails usage.
|
189
|
+
#
|
190
|
+
# @return [Array<Class>] Array of default permitted parameter types
|
191
|
+
# @private
|
99
192
|
def foa_default_permitted_types
|
100
193
|
[ActionController::Parameters, Hash]
|
101
194
|
end
|
102
195
|
|
103
|
-
# Convert string to
|
196
|
+
# Convert string timestamp to DateTime object.
|
197
|
+
# This is used for date/time filtering when the format is set to :datetime.
|
198
|
+
# By default, it expects Unix epoch timestamps as strings.
|
199
|
+
#
|
200
|
+
# This method can be overridden in controllers to support different date formats:
|
201
|
+
#
|
202
|
+
# @param string [String] The timestamp string to convert
|
203
|
+
# @return [DateTime] The parsed DateTime object
|
204
|
+
#
|
205
|
+
# @example Override in controller for custom date format
|
206
|
+
# class UsersController < ApplicationController
|
207
|
+
# private
|
208
|
+
#
|
209
|
+
# def foa_string_to_datetime(string)
|
210
|
+
# DateTime.strptime(string, '%Y-%m-%d %H:%M:%S')
|
211
|
+
# end
|
212
|
+
# end
|
213
|
+
#
|
214
|
+
# @example Default usage with epoch timestamps
|
215
|
+
# foa_string_to_datetime('1609459200') # => 2021-01-01 00:00:00 +0000
|
104
216
|
def foa_string_to_datetime(string)
|
105
|
-
DateTime.strptime(string,
|
217
|
+
DateTime.strptime(string, '%s')
|
106
218
|
end
|
107
219
|
end
|
108
220
|
|
221
|
+
# Automatically include FetcheableOnApi in all ActionController classes when Rails loads.
|
222
|
+
# This makes the filtering, sorting, and pagination functionality available
|
223
|
+
# to all controllers without requiring manual inclusion.
|
224
|
+
#
|
225
|
+
# @note This uses ActiveSupport's lazy loading mechanism to ensure ActionController
|
226
|
+
# is fully loaded before including the module.
|
109
227
|
ActiveSupport.on_load :action_controller do
|
110
228
|
include FetcheableOnApi
|
111
229
|
end
|
@@ -1,10 +1,24 @@
|
|
1
1
|
module FetcheableOnApi
|
2
2
|
module Generators
|
3
|
-
#
|
3
|
+
# Rails generator for creating FetcheableOnApi initializer file.
|
4
|
+
#
|
5
|
+
# This generator creates a configuration initializer file that allows
|
6
|
+
# developers to customize FetcheableOnApi settings for their application.
|
7
|
+
#
|
8
|
+
# @example Running the generator
|
9
|
+
# rails generate fetcheable_on_api:install
|
10
|
+
# # Creates: config/initializers/fetcheable_on_api.rb
|
11
|
+
#
|
12
|
+
# @since 0.1.0
|
4
13
|
class InstallGenerator < Rails::Generators::Base
|
5
14
|
source_root File.expand_path('../templates', __dir__)
|
6
15
|
desc 'Creates FetcheableOnApi initializer for your application'
|
7
16
|
|
17
|
+
# Copy the initializer template to the Rails application's config/initializers directory.
|
18
|
+
# The generated file contains configuration options with sensible defaults and
|
19
|
+
# documentation about available settings.
|
20
|
+
#
|
21
|
+
# @return [void]
|
8
22
|
def copy_initializer
|
9
23
|
template 'fetcheable_on_api_initializer.rb',
|
10
24
|
'config/initializers/fetcheable_on_api.rb'
|
@@ -1,3 +1,17 @@
|
|
1
|
+
# FetcheableOnApi configuration
|
2
|
+
#
|
3
|
+
# This initializer configures the FetcheableOnApi gem settings for your application.
|
4
|
+
# These settings affect the behavior of filtering, sorting, and pagination across
|
5
|
+
# all controllers that use the FetcheableOnApi module.
|
6
|
+
|
1
7
|
FetcheableOnApi.configure do |config|
|
8
|
+
# Default number of records per page when no page[size] parameter is provided.
|
9
|
+
# This affects the Pageable module when clients don't specify a page size.
|
10
|
+
#
|
11
|
+
# Examples:
|
12
|
+
# - With default (25): GET /users?page[number]=2 returns 25 records
|
13
|
+
# - With custom (50): GET /users?page[number]=2 returns 50 records
|
14
|
+
#
|
15
|
+
# Default: 25
|
2
16
|
config.pagination_default_size = 25
|
3
17
|
end
|