rest_framework 1.2.0 → 2.0.0.beta2
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 +109 -41
- data/VERSION +1 -1
- data/lib/rest_framework/controller/actions.rb +256 -0
- data/lib/rest_framework/controller/bulk.rb +15 -10
- data/lib/rest_framework/controller/crud.rb +10 -5
- data/lib/rest_framework/controller/openapi.rb +3 -2
- data/lib/rest_framework/controller.rb +231 -137
- data/lib/rest_framework/errors.rb +18 -1
- data/lib/rest_framework/filters/base_filter.rb +10 -0
- data/lib/rest_framework/filters/ordering_filter.rb +43 -23
- data/lib/rest_framework/filters/query_filter.rb +19 -5
- data/lib/rest_framework/filters/search_filter.rb +8 -8
- data/lib/rest_framework/paginators/page_number_paginator.rb +25 -9
- data/lib/rest_framework/routers.rb +52 -188
- data/lib/rest_framework/serializers/active_model_serializer_adapter_factory.rb +2 -2
- data/lib/rest_framework/serializers/base_serializer.rb +2 -2
- data/lib/rest_framework/serializers/native_serializer.rb +76 -24
- data/lib/rest_framework/utils.rb +32 -70
- data/lib/rest_framework/version.rb +8 -5
- data/lib/rest_framework.rb +7 -38
- metadata +5 -8
- data/lib/rest_framework/mixins/base_controller_mixin.rb +0 -12
- data/lib/rest_framework/mixins/bulk_model_controller_mixin.rb +0 -55
- data/lib/rest_framework/mixins/model_controller_mixin.rb +0 -110
- data/lib/rest_framework/mixins.rb +0 -7
|
@@ -1,51 +1,71 @@
|
|
|
1
1
|
# A filter backend which handles ordering of the recordset.
|
|
2
2
|
class RESTFramework::Filters::OrderingFilter < RESTFramework::Filters::BaseFilter
|
|
3
3
|
def _get_fields
|
|
4
|
-
|
|
4
|
+
# Always return a list of strings; `@controller.readable_columns_or_associations` already does.
|
|
5
|
+
@controller.class.ordering_fields&.map(&:to_s) || @controller.readable_columns_or_associations
|
|
5
6
|
end
|
|
6
7
|
|
|
7
|
-
# Convert ordering
|
|
8
|
+
# Convert the ordering param into an `[ordering, references]` pair: the ordering config for
|
|
9
|
+
# `order`/`reorder`, and the association names that must be joined for it to resolve.
|
|
8
10
|
def _get_ordering
|
|
9
11
|
return nil unless param = @controller.class.ordering_query_param.presence
|
|
10
12
|
|
|
11
13
|
# Ensure ordering_fields are strings since the split param will be strings.
|
|
12
14
|
fields = self._get_fields
|
|
13
|
-
order_string = @controller.
|
|
15
|
+
order_string = @controller.request.query_parameters[param]
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
# Reject nested-hash inputs like `?ordering[evil]=x` (Rack parses these into
|
|
18
|
+
# a Hash, which can't be split into ordering tokens).
|
|
19
|
+
return nil unless self.class._safe_query_value?(order_string)
|
|
20
|
+
return nil unless order_string.present?
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if field[0] == "-"
|
|
21
|
-
column = field[1..-1]
|
|
22
|
-
direction = :desc
|
|
23
|
-
else
|
|
24
|
-
column = field
|
|
25
|
-
direction = :asc
|
|
26
|
-
end
|
|
22
|
+
ordering = {}.with_indifferent_access
|
|
23
|
+
references = []
|
|
27
24
|
|
|
28
|
-
|
|
25
|
+
order_string = order_string.join(",") if order_string.is_a?(Array)
|
|
26
|
+
order_string.split(",").map(&:strip).each do |field|
|
|
27
|
+
if field[0] == "-"
|
|
28
|
+
column = field[1..-1]
|
|
29
|
+
direction = :desc
|
|
30
|
+
else
|
|
31
|
+
column = field
|
|
32
|
+
direction = :asc
|
|
33
|
+
end
|
|
29
34
|
|
|
35
|
+
# A plain, directly-allowlisted field.
|
|
36
|
+
if column.in?(fields)
|
|
30
37
|
ordering[column] = direction
|
|
38
|
+
next
|
|
31
39
|
end
|
|
32
40
|
|
|
33
|
-
|
|
41
|
+
# A dotted `association.sub_field` token. The root must be an allowlisted association field,
|
|
42
|
+
# and the sub-field must be one of that association's allowlisted fields. Otherwise a client
|
|
43
|
+
# could order by (and infer, via an ordering oracle) a column that is never serialized.
|
|
44
|
+
root, sub = column.split(".", 2)
|
|
45
|
+
next unless sub && root.in?(fields)
|
|
46
|
+
|
|
47
|
+
cfg = @controller.class.field_configuration[root]
|
|
48
|
+
next unless cfg && sub.in?(cfg[:fields] || [])
|
|
49
|
+
|
|
50
|
+
ordering[column] = direction
|
|
51
|
+
references << root.to_sym
|
|
34
52
|
end
|
|
35
53
|
|
|
36
|
-
nil
|
|
54
|
+
return nil if ordering.empty?
|
|
55
|
+
|
|
56
|
+
[ ordering, references ]
|
|
37
57
|
end
|
|
38
58
|
|
|
39
59
|
# Order data according to the request query parameters.
|
|
40
60
|
def filter_data(data)
|
|
41
|
-
ordering = self._get_ordering
|
|
42
|
-
|
|
61
|
+
ordering, references = self._get_ordering
|
|
62
|
+
return data unless ordering
|
|
43
63
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
end
|
|
64
|
+
# Join any referenced associations so dotted ordering keys resolve instead of raising.
|
|
65
|
+
data = data.includes(*references).references(*references) if references.present?
|
|
47
66
|
|
|
48
|
-
|
|
67
|
+
reorder = !@controller.class.ordering_no_reorder
|
|
68
|
+
data.send(reorder ? :reorder : :order, ordering)
|
|
49
69
|
end
|
|
50
70
|
end
|
|
51
71
|
|
|
@@ -37,9 +37,14 @@ class RESTFramework::Filters::QueryFilter < RESTFramework::Filters::BaseFilter
|
|
|
37
37
|
}.freeze
|
|
38
38
|
PREDICATES_REGEX = /^(.*)_(#{PREDICATES.keys.join("|")})$/
|
|
39
39
|
|
|
40
|
+
# Predicates whose value may be an array (e.g. `?id_in[]=1&id_in[]=2`). Every other predicate
|
|
41
|
+
# operates on a single scalar and skips array input, which would otherwise raise: `cont` in
|
|
42
|
+
# `sanitize_sql_like`, the range predicates while casting the endpoint.
|
|
43
|
+
ARRAY_PREDICATES = %i[in not].freeze
|
|
44
|
+
|
|
40
45
|
def _get_fields
|
|
41
|
-
# Always return a list of strings; `@controller.
|
|
42
|
-
@controller.class.filter_fields&.map(&:to_s) || @controller.
|
|
46
|
+
# Always return a list of strings; `@controller.readable_columns_or_associations` already does.
|
|
47
|
+
@controller.class.filter_fields&.map(&:to_s) || @controller.readable_columns_or_associations
|
|
43
48
|
end
|
|
44
49
|
|
|
45
50
|
# Helper to find a variation of a field using a predicate. For example, there could be a field
|
|
@@ -66,6 +71,11 @@ class RESTFramework::Filters::QueryFilter < RESTFramework::Filters::BaseFilter
|
|
|
66
71
|
pred_queries = []
|
|
67
72
|
|
|
68
73
|
base_query = @controller.request.query_parameters.map { |field, v|
|
|
74
|
+
# Skip params whose values aren't bind-safe (e.g. a user submitted
|
|
75
|
+
# `?field[evil]=x`, which Rack parses into a Hash). AR can't quote
|
|
76
|
+
# those, and the predicate lambdas below would also blow up on them.
|
|
77
|
+
next nil unless self.class._safe_query_value?(v)
|
|
78
|
+
|
|
69
79
|
# First, if field is a simple filterable field, return early.
|
|
70
80
|
if field.in?(fields)
|
|
71
81
|
next [ field, v ]
|
|
@@ -84,11 +94,11 @@ class RESTFramework::Filters::QueryFilter < RESTFramework::Filters::BaseFilter
|
|
|
84
94
|
if sub_field
|
|
85
95
|
next nil unless root_field.in?(fields)
|
|
86
96
|
|
|
87
|
-
|
|
88
|
-
if sub_field.in?(
|
|
97
|
+
association_fields = @controller.class.field_configuration[root_field][:fields] || []
|
|
98
|
+
if sub_field.in?(association_fields)
|
|
89
99
|
includes << root_field.to_sym
|
|
90
100
|
next [ field, v ]
|
|
91
|
-
elsif pred_sub_field && pred_sub_field.in?(
|
|
101
|
+
elsif pred_sub_field && pred_sub_field.in?(association_fields)
|
|
92
102
|
includes << root_field.to_sym
|
|
93
103
|
field = pred_field
|
|
94
104
|
else
|
|
@@ -103,6 +113,10 @@ class RESTFramework::Filters::QueryFilter < RESTFramework::Filters::BaseFilter
|
|
|
103
113
|
# value into a query that can be used in the ActiveRecord `where` API.
|
|
104
114
|
cfg = PREDICATES[predicate.to_sym]
|
|
105
115
|
if cfg.is_a?(Proc)
|
|
116
|
+
# Skip a scalar predicate given an array value (Rack parses `?field_cont[]=a&field_cont[]=b`
|
|
117
|
+
# into an array); only `in`/`not` accept arrays, the rest would raise.
|
|
118
|
+
next nil if v.is_a?(Array) && !predicate.to_sym.in?(ARRAY_PREDICATES)
|
|
119
|
+
|
|
106
120
|
pred_queries << cfg.call(field, v)
|
|
107
121
|
else
|
|
108
122
|
pred_queries << { field => cfg }
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
class RESTFramework::Filters::SearchFilter < RESTFramework::Filters::BaseFilter
|
|
2
2
|
def _get_fields
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
columns = @controller.class.model.column_names
|
|
8
|
-
@controller.get_fields.select { |f|
|
|
9
|
-
f.in?(RESTFramework.config.search_columns) && f.in?(columns)
|
|
10
|
-
}
|
|
3
|
+
# Always return a list of strings; `@controller.readable_columns` already does.
|
|
4
|
+
@controller.class.search_fields&.map(&:to_s) || (
|
|
5
|
+
@controller.readable_columns & RESTFramework.config.search_columns
|
|
6
|
+
)
|
|
11
7
|
end
|
|
12
8
|
|
|
13
9
|
# Filter data according to the request query parameters.
|
|
14
10
|
def filter_data(data)
|
|
15
11
|
search = @controller.request.query_parameters[@controller.class.search_query_param]
|
|
16
12
|
|
|
13
|
+
# Reject nested-hash inputs like `?search[evil]=x` (Rack parses these into a
|
|
14
|
+
# Hash, which `sanitize_sql_like` can't accept).
|
|
15
|
+
return data unless search.is_a?(String)
|
|
16
|
+
|
|
17
17
|
if search.present?
|
|
18
18
|
if fields = self._get_fields.presence
|
|
19
19
|
# MySQL doesn't support casting to VARCHAR, so we need to use CHAR instead.
|
|
@@ -4,12 +4,19 @@
|
|
|
4
4
|
class RESTFramework::Paginators::PageNumberPaginator < RESTFramework::Paginators::BasePaginator
|
|
5
5
|
def initialize(**kwargs)
|
|
6
6
|
super
|
|
7
|
-
# Exclude any `select` clauses since that would cause `count` to fail with a SQL `SyntaxError`.
|
|
8
|
-
@count = @data.except(:select).count
|
|
9
7
|
@page_size = self._page_size
|
|
8
|
+
@total_count = @controller.class.page_total_count
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
# Compute the total count (and total pages) unless disabled. On large tables `page_total_count`
|
|
11
|
+
# can be set to `false` to skip this `COUNT(*)` over the whole filtered set; `next` is then
|
|
12
|
+
# derived by fetching one extra record in `get_page`.
|
|
13
|
+
if @total_count
|
|
14
|
+
# Exclude any `select` clauses, since that would cause `count` to fail with a SQL
|
|
15
|
+
# `SyntaxError`.
|
|
16
|
+
@count = @data.except(:select).count
|
|
17
|
+
@total_pages = @count / @page_size
|
|
18
|
+
@total_pages += 1 if @count % @page_size != 0
|
|
19
|
+
end
|
|
13
20
|
end
|
|
14
21
|
|
|
15
22
|
def _page_size
|
|
@@ -17,7 +24,7 @@ class RESTFramework::Paginators::PageNumberPaginator < RESTFramework::Paginators
|
|
|
17
24
|
|
|
18
25
|
# Get from query param, if allowed.
|
|
19
26
|
if param = @controller.class.page_size_query_param
|
|
20
|
-
if raw = @controller.
|
|
27
|
+
if raw = @controller.request.query_parameters[param].presence
|
|
21
28
|
parsed = raw.to_i
|
|
22
29
|
page_size = parsed if parsed > 0
|
|
23
30
|
end
|
|
@@ -38,9 +45,9 @@ class RESTFramework::Paginators::PageNumberPaginator < RESTFramework::Paginators
|
|
|
38
45
|
|
|
39
46
|
# Get the page and return it so the caller can serialize it.
|
|
40
47
|
def get_page(page_number = nil)
|
|
41
|
-
# If page number isn't provided, infer from the params or use 1 as a fallback value.
|
|
48
|
+
# If page number isn't provided, infer from the query params or use 1 as a fallback value.
|
|
42
49
|
unless page_number
|
|
43
|
-
page_number = @controller&.
|
|
50
|
+
page_number = @controller&.request&.query_parameters&.[](@controller.class.page_query_param)
|
|
44
51
|
if page_number.blank?
|
|
45
52
|
page_number = 1
|
|
46
53
|
else
|
|
@@ -54,14 +61,23 @@ class RESTFramework::Paginators::PageNumberPaginator < RESTFramework::Paginators
|
|
|
54
61
|
|
|
55
62
|
# Get the data page and return it so the caller can serialize the data in the proper format.
|
|
56
63
|
page_index = @page_number - 1
|
|
57
|
-
|
|
64
|
+
offset = page_index * @page_size
|
|
65
|
+
|
|
66
|
+
# Without a total count we can't derive `next` from `total_pages`, so detect whether a further
|
|
67
|
+
# page exists with a cheap existence check (a `LIMIT 1` past this page) instead of a full count.
|
|
68
|
+
unless @total_count
|
|
69
|
+
@has_next = @data.except(:select).offset(offset + @page_size).exists?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
@data.limit(@page_size).offset(offset)
|
|
58
73
|
end
|
|
59
74
|
|
|
60
75
|
# Wrap the serialized page with appropriate metadata.
|
|
61
76
|
def get_paginated_response(serialized_page)
|
|
62
77
|
page_query_param = @controller.class.page_query_param
|
|
63
78
|
base_params = @controller.request.query_parameters.symbolize_keys
|
|
64
|
-
|
|
79
|
+
has_next = @total_count ? @page_number < @total_pages : @has_next
|
|
80
|
+
next_url = if has_next
|
|
65
81
|
@controller.url_for({ **base_params, page_query_param => @page_number + 1 })
|
|
66
82
|
end
|
|
67
83
|
previous_url = if @page_number > 1
|
|
@@ -2,221 +2,85 @@ require "action_dispatch/routing/mapper"
|
|
|
2
2
|
|
|
3
3
|
module ActionDispatch::Routing
|
|
4
4
|
class Mapper
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
name
|
|
10
|
-
if name == name.pluralize
|
|
11
|
-
name_reverse = name.singularize
|
|
12
|
-
else
|
|
13
|
-
name_reverse = name.pluralize
|
|
14
|
-
end
|
|
15
|
-
name += "Controller"
|
|
16
|
-
name_reverse += "Controller"
|
|
17
|
-
|
|
18
|
-
# Get scope for the class.
|
|
19
|
-
if @scope[:module]
|
|
20
|
-
mod = @scope[:module].to_s.camelize.constantize
|
|
21
|
-
else
|
|
22
|
-
mod = Object
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Convert class name to class.
|
|
26
|
-
begin
|
|
27
|
-
controller = mod.const_get(name)
|
|
28
|
-
rescue NameError
|
|
29
|
-
if fallback_reverse_pluralization
|
|
30
|
-
reraise = false
|
|
31
|
-
|
|
32
|
-
begin
|
|
33
|
-
controller = mod.const_get(name_reverse)
|
|
34
|
-
rescue NameError
|
|
35
|
-
reraise = true
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
if reraise
|
|
39
|
-
raise
|
|
40
|
-
end
|
|
41
|
-
else
|
|
42
|
-
raise
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
controller
|
|
5
|
+
# Resolve a controller class from a route name and the current scope. The name must match the
|
|
6
|
+
# controller exactly (camelized, plus `Controller`) — there is no pluralization fallback.
|
|
7
|
+
def _rrf_controller_class(name)
|
|
8
|
+
mod = @scope[:module] ? @scope[:module].to_s.camelize.constantize : Object
|
|
9
|
+
mod.const_get("#{name.to_s.camelize}Controller")
|
|
47
10
|
end
|
|
48
11
|
|
|
49
|
-
#
|
|
50
|
-
def
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
12
|
+
# Route each action from a controller's action store.
|
|
13
|
+
def _rrf_route_actions(actions)
|
|
14
|
+
actions.each_value do |spec|
|
|
15
|
+
# Delegated actions keep their declared action name (so routing and OpenAPI show the real
|
|
16
|
+
# name); `method_for_action` redirects dispatch to `rrf_delegate`, which needs the scope.
|
|
17
|
+
kwargs = spec.kwargs
|
|
18
|
+
if !spec.builtin && spec.metadata&.[](:delegate)
|
|
19
|
+
kwargs = kwargs.merge(rrf_delegate_scope: spec.type)
|
|
56
20
|
end
|
|
57
21
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
key = "#{@scope[:path]}/#{config[:path]}"
|
|
61
|
-
RESTFramework::EXTRA_ACTION_ROUTES.add(key)
|
|
62
|
-
RESTFramework::ROUTE_METADATA[key] = metadata if metadata
|
|
63
|
-
|
|
64
|
-
yield if block_given?
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Internal core implementation of the `rest_resource(s)` router, both singular and plural.
|
|
69
|
-
# @param default_singular [Boolean] the default plurality of the resource if the plurality is
|
|
70
|
-
# not otherwise defined by the controller
|
|
71
|
-
# @param name [Symbol] the resource name, from which path and controller are deduced by default
|
|
72
|
-
def _rest_resources(default_singular, name, **kwargs, &block)
|
|
73
|
-
controller = kwargs.delete(:controller) || name
|
|
74
|
-
if controller.is_a?(Class)
|
|
75
|
-
controller_class = controller
|
|
76
|
-
else
|
|
77
|
-
controller_class = self._get_controller_class(controller, pluralize: !default_singular)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Set controller if it's not explicitly set.
|
|
81
|
-
kwargs[:controller] = name unless kwargs[:controller]
|
|
82
|
-
|
|
83
|
-
# Passing `unscoped: true` will prevent a nested resource from being scoped.
|
|
84
|
-
unscoped = kwargs.delete(:unscoped)
|
|
85
|
-
|
|
86
|
-
# Determine plural/singular resource.
|
|
87
|
-
if !controller_class.singleton_controller.nil?
|
|
88
|
-
singular = controller_class.singleton_controller
|
|
89
|
-
else
|
|
90
|
-
singular = default_singular
|
|
91
|
-
end
|
|
92
|
-
resource_method = singular ? :resource : :resources
|
|
93
|
-
|
|
94
|
-
# Call either `resource` or `resources`, passing appropriate modifiers.
|
|
95
|
-
skip = RESTFramework::Utils.get_skipped_builtin_actions(controller_class, singular)
|
|
96
|
-
public_send(resource_method, name, except: skip, **kwargs) do
|
|
97
|
-
if controller_class.respond_to?(:extra_member_actions)
|
|
98
|
-
member do
|
|
99
|
-
self._route_extra_actions(controller_class.extra_member_actions)
|
|
100
|
-
end
|
|
22
|
+
spec.methods.each do |m|
|
|
23
|
+
public_send(m, spec.path, action: spec.name, **kwargs)
|
|
101
24
|
end
|
|
102
25
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
self._route_extra_actions(controller_class.extra_actions)
|
|
106
|
-
|
|
107
|
-
# Route extra RRF-defined actions.
|
|
108
|
-
RESTFramework::RRF_BUILTIN_ACTIONS.each do |action, methods|
|
|
109
|
-
next unless controller_class.method_defined?(action)
|
|
110
|
-
|
|
111
|
-
[ methods ].flatten.each do |m|
|
|
112
|
-
# Anchor the route since Rails 8.1 OPTIONS routes are non-anchored by default, which
|
|
113
|
-
# causes parent OPTIONS routes to greedily intercept sub-path requests.
|
|
114
|
-
public_send(m, "", action: action, anchor: true) if self.respond_to?(m)
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
# Route bulk actions, if configured. These require a model and are gated by the `bulk`
|
|
119
|
-
# attribute, and may be individually excluded via `excluded_actions`.
|
|
120
|
-
if controller_class.model && controller_class.bulk
|
|
121
|
-
bulk_exclude = controller_class.excluded_actions&.to_set || Set.new
|
|
122
|
-
RESTFramework::RRF_BUILTIN_BULK_ACTIONS.each do |action, methods|
|
|
123
|
-
next unless controller_class.method_defined?(action)
|
|
124
|
-
next if bulk_exclude.include?(action)
|
|
26
|
+
# Record non-builtin (extra) actions and their metadata for the browsable API / OpenAPI.
|
|
27
|
+
next if spec.builtin
|
|
125
28
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
public_send(m, "", action: action, anchor: true) if self.respond_to?(m)
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
if unscoped
|
|
136
|
-
yield if block_given?
|
|
137
|
-
else
|
|
138
|
-
scope(module: name, as: name) do
|
|
139
|
-
yield if block_given?
|
|
140
|
-
end
|
|
141
|
-
end
|
|
29
|
+
key = "#{@scope[:path]}/#{spec.path}"
|
|
30
|
+
RESTFramework::EXTRA_ACTION_ROUTES.add(key)
|
|
31
|
+
RESTFramework::ROUTE_METADATA[key] = spec.metadata if spec.metadata
|
|
142
32
|
end
|
|
143
33
|
end
|
|
144
34
|
|
|
145
|
-
#
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
35
|
+
# Route one or more controllers from their action stores. Plural model controllers get
|
|
36
|
+
# collection/member scopes; singular and non-model controllers route everything at the root.
|
|
37
|
+
# Passing several names condenses simple routes into one call; per-name options (`path:`, `as:`,
|
|
38
|
+
# `controller:`, and a block) only apply to a single name.
|
|
39
|
+
def rest_route(*names, **kwargs, &block)
|
|
40
|
+
if names.size > 1 && (block || (kwargs.keys & [ :path, :as, :controller ]).any?)
|
|
41
|
+
raise ArgumentError, "rest_route: options and a block require a single name"
|
|
149
42
|
end
|
|
150
|
-
end
|
|
151
43
|
|
|
152
|
-
|
|
153
|
-
def rest_resources(*names, **kwargs, &block)
|
|
154
|
-
names.each do |n|
|
|
155
|
-
self._rest_resources(false, n, **kwargs, &block)
|
|
156
|
-
end
|
|
44
|
+
names.each { |name| _rrf_rest_route(name, **kwargs, &block) }
|
|
157
45
|
end
|
|
158
46
|
|
|
159
|
-
# Route a controller
|
|
160
|
-
def
|
|
47
|
+
# Route a single controller from its action store.
|
|
48
|
+
def _rrf_rest_route(name, **kwargs)
|
|
161
49
|
controller = kwargs.delete(:controller) || name
|
|
162
|
-
route_root_to = kwargs.delete(:route_root_to)
|
|
163
50
|
if controller.is_a?(Class)
|
|
164
51
|
controller_class = controller
|
|
165
52
|
else
|
|
166
|
-
controller_class = self.
|
|
53
|
+
controller_class = self._rrf_controller_class(controller)
|
|
167
54
|
end
|
|
168
55
|
|
|
169
56
|
# Set controller if it's not explicitly set.
|
|
170
57
|
kwargs[:controller] = name unless kwargs[:controller]
|
|
171
58
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
# Anchor the route since Rails 8.1 OPTIONS routes are non-anchored by default, which
|
|
192
|
-
# causes parent OPTIONS routes to greedily intercept sub-path requests.
|
|
193
|
-
public_send(m, "", action: action, anchor: true) if self.respond_to?(m)
|
|
194
|
-
end
|
|
59
|
+
has_model = !!controller_class.model
|
|
60
|
+
singular = controller_class.singular
|
|
61
|
+
actions = controller_class.actions
|
|
62
|
+
member_actions = controller_class.member_actions
|
|
63
|
+
|
|
64
|
+
# Use `resources` (plural) for plural model controllers to get the member `:id` scope; use
|
|
65
|
+
# `resource` (singular) for everything else.
|
|
66
|
+
resource_method = (has_model && !singular) ? :resources : :resource
|
|
67
|
+
|
|
68
|
+
public_send(resource_method, name, only: [], **kwargs) do
|
|
69
|
+
if has_model
|
|
70
|
+
if singular
|
|
71
|
+
# Singular model controller: actions and member actions are the same.
|
|
72
|
+
self._rrf_route_actions(actions)
|
|
73
|
+
self._rrf_route_actions(member_actions)
|
|
74
|
+
else
|
|
75
|
+
# Plural model controller: route collection/member actions separately.
|
|
76
|
+
collection { self._rrf_route_actions(actions) }
|
|
77
|
+
member { self._rrf_route_actions(member_actions) }
|
|
195
78
|
end
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
if unscoped
|
|
199
|
-
yield if block_given?
|
|
200
79
|
else
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
end
|
|
80
|
+
# Non-model controller: only actions (there is no member `:id` scope).
|
|
81
|
+
self._rrf_route_actions(actions)
|
|
204
82
|
end
|
|
205
|
-
end
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
# Route a controller's `#root` to '/' in the current scope/namespace, along with other actions.
|
|
209
|
-
def rest_root(name = nil, **kwargs, &block)
|
|
210
|
-
# By default, use RootController#root.
|
|
211
|
-
root_action = kwargs.delete(:action) || :root
|
|
212
|
-
controller = kwargs.delete(:controller) || name || :root
|
|
213
|
-
|
|
214
|
-
# Remove path if name is nil (routing to the root of current namespace).
|
|
215
|
-
unless name
|
|
216
|
-
kwargs[:path] = ""
|
|
217
|
-
end
|
|
218
83
|
|
|
219
|
-
rest_route(controller, route_root_to: root_action, **kwargs) do
|
|
220
84
|
yield if block_given?
|
|
221
85
|
end
|
|
222
86
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# This is a helper factory to wrap an ActiveModelSerializer to provide a `serialize` method which
|
|
2
2
|
# accepts both collections and individual records. Use `.for` to build adapters.
|
|
3
|
-
# :
|
|
3
|
+
# simplecov:disable
|
|
4
4
|
class RESTFramework::Serializers::ActiveModelSerializerAdapterFactory
|
|
5
5
|
def self.for(active_model_serializer)
|
|
6
6
|
Class.new(active_model_serializer) do
|
|
@@ -14,7 +14,7 @@ class RESTFramework::Serializers::ActiveModelSerializerAdapterFactory
|
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
|
-
# :
|
|
17
|
+
# simplecov:enable
|
|
18
18
|
|
|
19
19
|
# Alias for convenience.
|
|
20
20
|
# rubocop:disable Layout/LineLength
|
|
@@ -16,7 +16,7 @@ class RESTFramework::Serializers::BaseSerializer
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
# Synonym for `serialize` for compatibility with `active_model_serializers`.
|
|
19
|
-
# :
|
|
19
|
+
# simplecov:disable
|
|
20
20
|
def serializable_hash(*args)
|
|
21
21
|
self.serialize(*args)
|
|
22
22
|
end
|
|
@@ -35,7 +35,7 @@ class RESTFramework::Serializers::BaseSerializer
|
|
|
35
35
|
def associations(*args, **kwargs)
|
|
36
36
|
[]
|
|
37
37
|
end
|
|
38
|
-
# :
|
|
38
|
+
# simplecov:enable
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
# Alias for convenience.
|