rest_framework 0.9.16 → 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.
- checksums.yaml +4 -4
- data/README.md +3 -3
- data/VERSION +1 -1
- data/app/views/rest_framework/routes_and_forms/_html_form.html.erb +6 -6
- data/app/views/rest_framework/routes_and_forms/_raw_form.html.erb +1 -1
- data/app/views/rest_framework/routes_and_forms/routes/_route.html.erb +1 -1
- data/lib/rest_framework/errors/{nil_passed_to_api_response_error.rb → nil_passed_to_render_api_error.rb} +3 -3
- data/lib/rest_framework/errors.rb +1 -1
- data/lib/rest_framework/filters/ordering_filter.rb +4 -7
- data/lib/rest_framework/filters/query_filter.rb +1 -4
- data/lib/rest_framework/filters/ransack_filter.rb +4 -4
- data/lib/rest_framework/filters/search_filter.rb +3 -6
- data/lib/rest_framework/mixins/base_controller_mixin.rb +93 -97
- data/lib/rest_framework/mixins/bulk_model_controller_mixin.rb +7 -7
- data/lib/rest_framework/mixins/model_controller_mixin.rb +256 -222
- data/lib/rest_framework/paginators/page_number_paginator.rb +12 -11
- data/lib/rest_framework/routers.rb +11 -1
- data/lib/rest_framework/serializers/native_serializer.rb +24 -14
- data/lib/rest_framework/utils.rb +51 -40
- metadata +3 -3
@@ -13,34 +13,35 @@ class RESTFramework::Paginators::PageNumberPaginator < RESTFramework::Paginators
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def _page_size
|
16
|
-
page_size =
|
16
|
+
page_size = 1
|
17
17
|
|
18
18
|
# Get from context, if allowed.
|
19
|
-
if @controller.page_size_query_param
|
20
|
-
if page_size = @controller.params[
|
19
|
+
if param = @controller.class.page_size_query_param
|
20
|
+
if page_size = @controller.params[param].presence
|
21
21
|
page_size = page_size.to_i
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
25
|
# Otherwise, get from config.
|
26
|
-
if !page_size && @controller.page_size
|
27
|
-
page_size = @controller.page_size
|
26
|
+
if !page_size && @controller.class.page_size
|
27
|
+
page_size = @controller.class.page_size.to_i
|
28
28
|
end
|
29
29
|
|
30
30
|
# Ensure we don't exceed the max page size.
|
31
|
-
|
32
|
-
|
31
|
+
max_page_size = @controller.class.max_page_size&.to_i
|
32
|
+
if max_page_size && page_size > max_page_size
|
33
|
+
page_size = max_page_size
|
33
34
|
end
|
34
35
|
|
35
36
|
# Ensure we return at least 1.
|
36
|
-
return page_size
|
37
|
+
return [page_size, 1].max
|
37
38
|
end
|
38
39
|
|
39
40
|
# Get the page and return it so the caller can serialize it.
|
40
41
|
def get_page(page_number=nil)
|
41
42
|
# If page number isn't provided, infer from the params or use 1 as a fallback value.
|
42
43
|
unless page_number
|
43
|
-
page_number = @controller&.params&.[](@controller.page_query_param&.to_sym)
|
44
|
+
page_number = @controller&.params&.[](@controller.class.page_query_param&.to_sym)
|
44
45
|
if page_number.blank?
|
45
46
|
page_number = 1
|
46
47
|
else
|
@@ -57,9 +58,9 @@ class RESTFramework::Paginators::PageNumberPaginator < RESTFramework::Paginators
|
|
57
58
|
return @data.limit(@page_size).offset(page_index * @page_size)
|
58
59
|
end
|
59
60
|
|
60
|
-
# Wrap the serialized page with appropriate metadata.
|
61
|
+
# Wrap the serialized page with appropriate metadata.
|
61
62
|
def get_paginated_response(serialized_page)
|
62
|
-
page_query_param = @controller.page_query_param
|
63
|
+
page_query_param = @controller.class.page_query_param
|
63
64
|
base_params = @controller.params.to_unsafe_h
|
64
65
|
next_url = if @page_number < @total_pages
|
65
66
|
@controller.url_for({**base_params, page_query_param => @page_number + 1})
|
@@ -27,7 +27,17 @@ module ActionDispatch::Routing
|
|
27
27
|
controller = mod.const_get(name)
|
28
28
|
rescue NameError
|
29
29
|
if fallback_reverse_pluralization
|
30
|
-
|
30
|
+
reraise = false
|
31
|
+
|
32
|
+
begin
|
33
|
+
controller = mod.const_get(name_reverse)
|
34
|
+
rescue
|
35
|
+
reraise = true
|
36
|
+
end
|
37
|
+
|
38
|
+
if reraise
|
39
|
+
raise
|
40
|
+
end
|
31
41
|
else
|
32
42
|
raise
|
33
43
|
end
|
@@ -55,12 +55,12 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
55
55
|
return nil unless @controller
|
56
56
|
|
57
57
|
if @many == true
|
58
|
-
controller_serializer = @controller.
|
58
|
+
controller_serializer = @controller.class.native_serializer_plural_config
|
59
59
|
elsif @many == false
|
60
|
-
controller_serializer = @controller.
|
60
|
+
controller_serializer = @controller.class.native_serializer_singular_config
|
61
61
|
end
|
62
62
|
|
63
|
-
return controller_serializer || @controller.
|
63
|
+
return controller_serializer || @controller.class.native_serializer_config
|
64
64
|
end
|
65
65
|
|
66
66
|
# Filter a single subconfig for specific keys. By default, keys from `fields` are removed from the
|
@@ -114,8 +114,8 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
114
114
|
def filter_from_request(cfg)
|
115
115
|
return cfg unless @controller
|
116
116
|
|
117
|
-
except_param = @controller.
|
118
|
-
only_param = @controller.
|
117
|
+
except_param = @controller.class.native_serializer_except_query_param
|
118
|
+
only_param = @controller.class.native_serializer_only_query_param
|
119
119
|
if except_param && except = @controller.request&.query_parameters&.[](except_param).presence
|
120
120
|
if except = except.split(",").map(&:strip).map(&:to_sym).presence
|
121
121
|
# Filter `only`, `except` (additive), `include`, `methods`, and `serializer_methods`.
|
@@ -160,10 +160,10 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
160
160
|
def _get_associations_limit
|
161
161
|
return @_get_associations_limit if defined?(@_get_associations_limit)
|
162
162
|
|
163
|
-
limit = @controller&.native_serializer_associations_limit
|
163
|
+
limit = @controller&.class&.native_serializer_associations_limit
|
164
164
|
|
165
165
|
# Extract the limit from the query parameters if it's set.
|
166
|
-
if query_param = @controller&.native_serializer_associations_limit_query_param
|
166
|
+
if query_param = @controller&.class&.native_serializer_associations_limit_query_param
|
167
167
|
if @controller.request.query_parameters.key?(query_param)
|
168
168
|
query_limit = @controller.request.query_parameters[query_param].to_i
|
169
169
|
if query_limit > 0
|
@@ -195,7 +195,7 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
195
195
|
attachment_reflections = @model.attachment_reflections
|
196
196
|
|
197
197
|
fields.each do |f|
|
198
|
-
field_config = @controller.class.
|
198
|
+
field_config = @controller.class.field_configuration[f]
|
199
199
|
next if field_config[:write_only]
|
200
200
|
|
201
201
|
if f.in?(column_names)
|
@@ -226,7 +226,7 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
226
226
|
#
|
227
227
|
# # Even though we use a serializer method, if the count will later be added, then put
|
228
228
|
# # this field into the includes_map.
|
229
|
-
# if @controller.native_serializer_include_associations_count
|
229
|
+
# if @controller.class.native_serializer_include_associations_count
|
230
230
|
# includes_map[f] = f.to_sym
|
231
231
|
# end
|
232
232
|
else
|
@@ -235,7 +235,7 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
235
235
|
end
|
236
236
|
|
237
237
|
# If we need to include the association count, then add it here.
|
238
|
-
if @controller.native_serializer_include_associations_count
|
238
|
+
if @controller.class.native_serializer_include_associations_count
|
239
239
|
method_name = "#{f}.count"
|
240
240
|
serializer_methods[method_name] = method_name
|
241
241
|
self.define_singleton_method(method_name) do |record|
|
@@ -246,28 +246,38 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
246
246
|
includes[f] = sub_config
|
247
247
|
includes_map[f] = f.to_sym
|
248
248
|
end
|
249
|
-
elsif ref = reflections["rich_text_#{f}"]
|
249
|
+
elsif @controller.class.enable_action_text && ref = reflections["rich_text_#{f}"]
|
250
250
|
# ActionText Integration: Define rich text serializer method.
|
251
251
|
includes_map[f] = :"rich_text_#{f}"
|
252
252
|
serializer_methods[f] = f
|
253
253
|
self.define_singleton_method(f) do |record|
|
254
254
|
next record.send(f).to_s
|
255
255
|
end
|
256
|
-
elsif ref = attachment_reflections[f]
|
256
|
+
elsif @controller.class.enable_active_storage && ref = attachment_reflections[f]
|
257
257
|
# ActiveStorage Integration: Define attachment serializer method.
|
258
258
|
if ref.macro == :has_one_attached
|
259
259
|
serializer_methods[f] = f
|
260
260
|
includes_map[f] = {"#{f}_attachment": :blob}
|
261
261
|
self.define_singleton_method(f) do |record|
|
262
262
|
attached = record.send(f)
|
263
|
-
next attached.attachment ? {
|
263
|
+
next attached.attachment ? {
|
264
|
+
filename: attached.filename,
|
265
|
+
signed_id: attached.signed_id,
|
266
|
+
url: attached.url,
|
267
|
+
} : nil
|
264
268
|
end
|
265
269
|
elsif ref.macro == :has_many_attached
|
266
270
|
serializer_methods[f] = f
|
267
271
|
includes_map[f] = {"#{f}_attachments": :blob}
|
268
272
|
self.define_singleton_method(f) do |record|
|
269
273
|
# Iterating the collection yields attachment objects.
|
270
|
-
next record.send(f).map { |a|
|
274
|
+
next record.send(f).map { |a|
|
275
|
+
{
|
276
|
+
filename: a.filename,
|
277
|
+
signed_id: a.signed_id,
|
278
|
+
url: a.url,
|
279
|
+
}
|
280
|
+
}
|
271
281
|
end
|
272
282
|
end
|
273
283
|
elsif @model.method_defined?(f)
|
data/lib/rest_framework/utils.rb
CHANGED
@@ -1,14 +1,12 @@
|
|
1
1
|
module RESTFramework::Utils
|
2
2
|
HTTP_VERB_ORDERING = %w(GET POST PUT PATCH DELETE OPTIONS HEAD)
|
3
3
|
|
4
|
-
# Convert `extra_actions` hash to a consistent format: `{path:, methods:, kwargs:}
|
5
|
-
# additional metadata fields.
|
4
|
+
# Convert `extra_actions` hash to a consistent format: `{path:, methods:, kwargs:}`.
|
6
5
|
#
|
7
6
|
# If a controller is provided, labels will be added to any metadata fields.
|
8
7
|
def self.parse_extra_actions(extra_actions, controller: nil)
|
9
8
|
return (extra_actions || {}).map { |k, v|
|
10
9
|
path = k
|
11
|
-
metadata = {}
|
12
10
|
|
13
11
|
# Convert structure to path/methods/kwargs.
|
14
12
|
if v.is_a?(Hash) # Allow kwargs to be used to define path differently from the key.
|
@@ -19,11 +17,9 @@ module RESTFramework::Utils
|
|
19
17
|
methods = v.delete(:method)
|
20
18
|
end
|
21
19
|
|
22
|
-
#
|
23
|
-
metadata = v
|
24
|
-
|
25
|
-
# Add label to fields.
|
26
|
-
if controller && metadata[:fields]
|
20
|
+
# Add label to metadata fields, if any exist.
|
21
|
+
metadata = v[:metadata]
|
22
|
+
if controller && metadata&.key?(:fields)
|
27
23
|
metadata[:fields] = metadata[:fields].map { |f|
|
28
24
|
[f, {}]
|
29
25
|
}.to_h if metadata[:fields].is_a?(Array)
|
@@ -56,8 +52,6 @@ module RESTFramework::Utils
|
|
56
52
|
path: path,
|
57
53
|
methods: methods,
|
58
54
|
kwargs: kwargs,
|
59
|
-
type: :extra,
|
60
|
-
metadata: metadata.presence,
|
61
55
|
}.compact,
|
62
56
|
]
|
63
57
|
}.to_h
|
@@ -91,25 +85,28 @@ module RESTFramework::Utils
|
|
91
85
|
current_levels = current_path.count("/")
|
92
86
|
current_comparable_path = %r{^#{Regexp.quote(self.comparable_path(current_path))}(/|$)}
|
93
87
|
|
94
|
-
#
|
95
|
-
|
96
|
-
route_props = {
|
97
|
-
with_path_args: ->(r) {
|
98
|
-
r.format(r.required_parts.each_with_index.map { |p, i| [p, path_args[i]] }.to_h)
|
99
|
-
},
|
100
|
-
}
|
88
|
+
# Get current route path parameters.
|
89
|
+
path_params = current_route.required_parts.map { |n| request.path_parameters[n] }
|
101
90
|
|
102
91
|
# Return routes that match our current route subdomain/pattern, grouped by controller. We
|
103
92
|
# precompute certain properties of the route for performance.
|
104
|
-
return
|
93
|
+
return application_routes.routes.select { |r|
|
105
94
|
# We `select` first to avoid unnecessarily calculating metadata for routes we don't even want
|
106
95
|
# to show.
|
107
96
|
(r.defaults[:subdomain].blank? || r.defaults[:subdomain] == request.subdomain) &&
|
108
|
-
|
109
|
-
|
110
|
-
|
97
|
+
current_comparable_path.match?(self.comparable_path(r.path.spec.to_s)) &&
|
98
|
+
r.defaults[:controller].present? &&
|
99
|
+
r.defaults[:action].present?
|
111
100
|
}.map { |r|
|
112
101
|
path = r.path.spec.to_s.gsub("(.:format)", "")
|
102
|
+
|
103
|
+
# Starts at the number of levels in current path, and removes the `(.:format)` at the end.
|
104
|
+
relative_path = path.split("/")[current_levels..]&.join("/").presence || "/"
|
105
|
+
|
106
|
+
# This path is what would need to be concatenated onto the current path to get to the
|
107
|
+
# destination path.
|
108
|
+
concat_path = relative_path.gsub(/^[^\/]*/, "").presence || "/"
|
109
|
+
|
113
110
|
levels = path.count("/")
|
114
111
|
matches_path = current_path == path
|
115
112
|
matches_params = r.required_parts.length == current_route.required_parts.length
|
@@ -118,8 +115,11 @@ module RESTFramework::Utils
|
|
118
115
|
route: r,
|
119
116
|
verb: r.verb,
|
120
117
|
path: path,
|
121
|
-
|
122
|
-
|
118
|
+
path_with_params: r.format(
|
119
|
+
r.required_parts.each_with_index.map { |p, i| [p, path_params[i]] }.to_h,
|
120
|
+
),
|
121
|
+
relative_path: relative_path,
|
122
|
+
concat_path: concat_path,
|
123
123
|
controller: r.defaults[:controller].presence,
|
124
124
|
action: r.defaults[:action].presence,
|
125
125
|
matches_path: matches_path,
|
@@ -153,16 +153,21 @@ module RESTFramework::Utils
|
|
153
153
|
end
|
154
154
|
|
155
155
|
# Parse fields hashes.
|
156
|
-
def self.parse_fields_hash(
|
157
|
-
parsed_fields =
|
158
|
-
model ? self.fields_for(
|
156
|
+
def self.parse_fields_hash(h, model, exclude_associations:, action_text:, active_storage:)
|
157
|
+
parsed_fields = h[:only] || (
|
158
|
+
model ? self.fields_for(
|
159
|
+
model,
|
160
|
+
exclude_associations: exclude_associations,
|
161
|
+
action_text: action_text,
|
162
|
+
active_storage: active_storage,
|
163
|
+
) : []
|
159
164
|
)
|
160
|
-
parsed_fields +=
|
161
|
-
parsed_fields -=
|
162
|
-
parsed_fields -=
|
165
|
+
parsed_fields += h[:include].map(&:to_s) if h[:include]
|
166
|
+
parsed_fields -= h[:exclude].map(&:to_s) if h[:exclude]
|
167
|
+
parsed_fields -= h[:except].map(&:to_s) if h[:except]
|
163
168
|
|
164
169
|
# Warn for any unknown keys.
|
165
|
-
(
|
170
|
+
(h.keys - [:only, :except, :include, :exclude]).each do |k|
|
166
171
|
Rails.logger.warn("RRF: Unknown key in fields hash: #{k}")
|
167
172
|
end
|
168
173
|
|
@@ -173,16 +178,24 @@ module RESTFramework::Utils
|
|
173
178
|
# Get the fields for a given model, including not just columns (which includes
|
174
179
|
# foreign keys), but also associations. Note that we always return an array of
|
175
180
|
# strings, not symbols.
|
176
|
-
def self.fields_for(model, exclude_associations:
|
181
|
+
def self.fields_for(model, exclude_associations:, action_text:, active_storage:)
|
177
182
|
foreign_keys = model.reflect_on_all_associations(:belongs_to).map(&:foreign_key)
|
178
183
|
base_fields = model.column_names.reject { |c| c.in?(foreign_keys) }
|
179
184
|
|
180
185
|
return base_fields if exclude_associations
|
181
186
|
|
182
|
-
#
|
183
|
-
|
187
|
+
# ActionText Integration: Determine the normalized field names for action text attributes.
|
188
|
+
atf = action_text ? model.reflect_on_all_associations(:has_one).collect(&:name).select { |n|
|
189
|
+
n.to_s.start_with?("rich_text_")
|
190
|
+
}.map { |n| n.to_s.delete_prefix("rich_text_") } : []
|
191
|
+
|
192
|
+
# ActiveStorage Integration: Determine the normalized field names for active storage attributes.
|
193
|
+
asf = active_storage ? model.attachment_reflections.keys : []
|
194
|
+
|
195
|
+
# Associations:
|
196
|
+
associations = model.reflections.map { |association, ref|
|
184
197
|
# Ignore associations for which we have custom integrations.
|
185
|
-
if ref.class_name.in?(%w(ActiveStorage::Attachment ActiveStorage::Blob
|
198
|
+
if ref.class_name.in?(%w(ActionText::RichText ActiveStorage::Attachment ActiveStorage::Blob))
|
186
199
|
next nil
|
187
200
|
end
|
188
201
|
|
@@ -193,11 +206,9 @@ module RESTFramework::Utils
|
|
193
206
|
end
|
194
207
|
|
195
208
|
next association
|
196
|
-
}.compact
|
197
|
-
|
198
|
-
|
199
|
-
n.to_s.delete_prefix("rich_text_")
|
200
|
-
} + model.attachment_reflections.keys
|
209
|
+
}.compact
|
210
|
+
|
211
|
+
return base_fields + associations + atf + asf
|
201
212
|
end
|
202
213
|
|
203
214
|
# Get the sub-fields that may be serialized and filtered/ordered for a reflection.
|
@@ -223,7 +234,7 @@ module RESTFramework::Utils
|
|
223
234
|
end
|
224
235
|
|
225
236
|
# Get a field's id/ids variation.
|
226
|
-
def self.
|
237
|
+
def self.id_field_for(field, reflection)
|
227
238
|
if reflection.collection?
|
228
239
|
return "#{field.singularize}_ids"
|
229
240
|
elsif reflection.belongs_to?
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rest_framework
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.11.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gregory N. Schmit
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-12-
|
11
|
+
date: 2024-12-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -54,7 +54,7 @@ files:
|
|
54
54
|
- lib/rest_framework/engine.rb
|
55
55
|
- lib/rest_framework/errors.rb
|
56
56
|
- lib/rest_framework/errors/base_error.rb
|
57
|
-
- lib/rest_framework/errors/
|
57
|
+
- lib/rest_framework/errors/nil_passed_to_render_api_error.rb
|
58
58
|
- lib/rest_framework/errors/unknown_model_error.rb
|
59
59
|
- lib/rest_framework/filters.rb
|
60
60
|
- lib/rest_framework/filters/base_filter.rb
|