rest_framework 2.0.0.beta3 → 2.0.0.beta5
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 +14 -11
- data/VERSION +1 -1
- data/lib/rest_framework/controller.rb +91 -36
- data/lib/rest_framework/routers.rb +18 -14
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e483f8e7b422c2a45545f8c6e5d6313cc8a3df9dc286e76d4a6f09b9c717d9bf
|
|
4
|
+
data.tar.gz: 16d95dda8928c50b01cbc9cc291778928c9023d77e05c35f51cc9c8411a4c7d0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 05ae9adf0e303bc8aa23acda8d33f8e2026de7e123ec57a1c51dc030b92b25caed62d872badf76b9c8e3c0f549c5899b59c3352b2620c767b61d4e1b1ac34672
|
|
7
|
+
data.tar.gz: ca6913825b5eaf6c6545359c494b8b38eae2deb87e285ed8d1e761ab38700f1f0e2be4469d3ab6c604bc98d30918b4c1c40ce0098d32027e0bed52db51c2b049
|
data/README.md
CHANGED
|
@@ -137,18 +137,20 @@ end
|
|
|
137
137
|
|
|
138
138
|
### Routing
|
|
139
139
|
|
|
140
|
-
Use `
|
|
141
|
-
`resources` for a plural model controller and
|
|
142
|
-
|
|
143
|
-
|
|
140
|
+
Use `rest_resource` to route a controller (`rest_resources` routes several that share options). They
|
|
141
|
+
wrap Rails' `resource` / `resources` routers, picking `resources` for a plural model controller and
|
|
142
|
+
`resource` otherwise — so plurality follows the controller's config, not the helper name. Built-in
|
|
143
|
+
and extra actions are routed automatically: a controller with a `model` gets the full CRUD set; a
|
|
144
|
+
modelless controller is routed at its root (its `index`, which renders `index_content`). A block
|
|
145
|
+
nests resources, and a nested recordset is auto-scoped to its parent (`/movies/:movie_id/genres` →
|
|
146
|
+
`Movie.find(params[:movie_id]).genres`).
|
|
144
147
|
|
|
145
148
|
```ruby
|
|
146
149
|
Rails.application.routes.draw do
|
|
147
|
-
|
|
150
|
+
rest_resource :api # `ApiController` serves the `/api` root.
|
|
148
151
|
|
|
149
152
|
namespace :api do
|
|
150
|
-
|
|
151
|
-
rest_route :users
|
|
153
|
+
rest_resources :movies, :users
|
|
152
154
|
end
|
|
153
155
|
end
|
|
154
156
|
```
|
|
@@ -181,9 +183,9 @@ changes; the migration checklist that follows walks through updating an existing
|
|
|
181
183
|
- **Declarative actions** — `add_action` / `remove_action` declare extra routes with an explicit
|
|
182
184
|
`member` / `collection` scope and per-declaration propagation, and can disable built-ins too,
|
|
183
185
|
replacing the `extra_actions` config hashes.
|
|
184
|
-
- **
|
|
185
|
-
|
|
186
|
-
`
|
|
186
|
+
- **Simpler routing** — `rest_resource` routes one controller and `rest_resources` routes several
|
|
187
|
+
(plurality of the *routes* follows the controller's config, not the helper name); `rest_root` and
|
|
188
|
+
`rest_route` are gone, and nested resources are auto-scoped to their parent.
|
|
187
189
|
- **Action delegation** — mark an action `metadata: { delegate: true }` to dispatch it to a model
|
|
188
190
|
class method (collection) or record method (member), passing query params through as args/kwargs.
|
|
189
191
|
- **Consumer-driven association queries** (opt-in via `enable_association_queries`) — clients can
|
|
@@ -218,7 +220,8 @@ See the guide for details on each item.
|
|
|
218
220
|
- [ ] Convert `extra_actions` / `extra_member_actions` hashes to `add_action` / `remove_action`.
|
|
219
221
|
- [ ] Render custom actions with `render(api: ...)`, replacing the older `api_response(...)` /
|
|
220
222
|
`render_api(...)`.
|
|
221
|
-
- [ ] Replace `
|
|
223
|
+
- [ ] Replace `rest_root` and `rest_route` with `rest_resource` (single controller) or
|
|
224
|
+
`rest_resources` (several) — plurality of the routes now comes from controller config.
|
|
222
225
|
- [ ] Fold any dedicated root controller into the namespace's base controller, which now serves the
|
|
223
226
|
root via `index_content` (the standalone `root` action and `rest_root` are gone).
|
|
224
227
|
- [ ] Rename `singleton_controller` → `singular`.
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.0.0.
|
|
1
|
+
2.0.0.beta5
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# module rather than defining a separate submodule.
|
|
4
4
|
module RESTFramework::Controller
|
|
5
5
|
RRF_BASE_CONFIG = {
|
|
6
|
+
model: nil,
|
|
6
7
|
singular: nil,
|
|
7
8
|
|
|
8
9
|
# Options related to metadata and display.
|
|
@@ -12,10 +13,6 @@ module RESTFramework::Controller
|
|
|
12
13
|
inflect_acronyms: RESTFramework.config.inflect_acronyms,
|
|
13
14
|
openapi_include_children: false,
|
|
14
15
|
|
|
15
|
-
# Options related to models.
|
|
16
|
-
model: nil,
|
|
17
|
-
recordset: nil,
|
|
18
|
-
|
|
19
16
|
# Bulk configuration.
|
|
20
17
|
#
|
|
21
18
|
# When `bulk` is truthy, it enables the default bulk behavior (`:default`), which is per-record
|
|
@@ -98,6 +95,10 @@ module RESTFramework::Controller
|
|
|
98
95
|
# Option for `recordset.create` vs `Model.create` behavior.
|
|
99
96
|
create_from_recordset: true,
|
|
100
97
|
|
|
98
|
+
# Options for scoped nested routing.
|
|
99
|
+
scope_nested_by_parent: true,
|
|
100
|
+
scope_nested_through_controllers: true,
|
|
101
|
+
|
|
101
102
|
# Options related to serialization.
|
|
102
103
|
rescue_unknown_format_with: :json,
|
|
103
104
|
serializer_class: nil,
|
|
@@ -651,6 +652,15 @@ module RESTFramework::Controller
|
|
|
651
652
|
self.get_fields.reject { |f| cfg[f]&.[](:write_only) }
|
|
652
653
|
end
|
|
653
654
|
|
|
655
|
+
# The fields a client may write (create/update): `get_fields` minus read_only fields. Excluding
|
|
656
|
+
# them here — before strong parameters expand associations into `_id`/`_ids`/`_attributes` keys —
|
|
657
|
+
# keeps a read_only association from ever producing a permitted (and otherwise unstrippable, since
|
|
658
|
+
# those keys don't match a field name) assignment key.
|
|
659
|
+
def writable_fields
|
|
660
|
+
cfg = self.class.field_configuration
|
|
661
|
+
self.get_fields.reject { |f| cfg[f]&.[](:read_only) }
|
|
662
|
+
end
|
|
663
|
+
|
|
654
664
|
# `readable_fields` restricted to real columns, for query surfaces that build SQL directly
|
|
655
665
|
# (find_by, search) and would raise on a virtual/method field.
|
|
656
666
|
def readable_columns
|
|
@@ -673,12 +683,14 @@ module RESTFramework::Controller
|
|
|
673
683
|
@_get_allowed_parameters = self.class.allowed_parameters
|
|
674
684
|
return @_get_allowed_parameters if @_get_allowed_parameters
|
|
675
685
|
|
|
676
|
-
# Assemble strong parameters
|
|
677
|
-
#
|
|
686
|
+
# Assemble strong parameters from writable fields only, so read-only fields never produce a
|
|
687
|
+
# permitted key — including the `_id`/`_ids`/`_attributes` variations an association expands
|
|
688
|
+
# into, which a later key-name-based filter couldn't catch. Bulk update re-permits the primary
|
|
689
|
+
# key (see `get_body_params`) since it needs it to locate each record.
|
|
678
690
|
variations = []
|
|
679
691
|
hash_variations = {}
|
|
680
692
|
reflections = self.class.model.reflections
|
|
681
|
-
@_get_allowed_parameters = self.
|
|
693
|
+
@_get_allowed_parameters = self.writable_fields.map { |f|
|
|
682
694
|
f = f.to_s
|
|
683
695
|
config = self.class.field_configuration[f]
|
|
684
696
|
|
|
@@ -823,47 +835,90 @@ module RESTFramework::Controller
|
|
|
823
835
|
body_params[k].unshift(*v)
|
|
824
836
|
end
|
|
825
837
|
|
|
826
|
-
# Filter read-only fields. For bulk actions the permitted structure is `{ _json: [...] }`, so we
|
|
827
|
-
# strip read-only keys from each element rather than the top-level hash (whose only key is
|
|
828
|
-
# `_json`). Bulk update keeps the primary key, which it needs to locate each record.
|
|
829
|
-
if bulk_action
|
|
830
|
-
keep = bulk_action == :update ? [ pk.to_s ] : []
|
|
831
|
-
body_params[:_json]&.each do |element|
|
|
832
|
-
next unless element.is_a?(ActionController::Parameters)
|
|
833
|
-
|
|
834
|
-
self._rrf_strip_read_only_fields(element, keep: keep)
|
|
835
|
-
end
|
|
836
|
-
else
|
|
837
|
-
self._rrf_strip_read_only_fields(body_params)
|
|
838
|
-
end
|
|
839
|
-
|
|
840
838
|
body_params
|
|
841
839
|
end
|
|
842
840
|
alias_method :get_create_params, :get_body_params
|
|
843
841
|
alias_method :get_update_params, :get_body_params
|
|
844
842
|
alias_method :get_destroy_params, :get_body_params
|
|
845
843
|
|
|
846
|
-
#
|
|
847
|
-
#
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
844
|
+
# Get the set of records this controller has access to. Override this to scope records (e.g. to
|
|
845
|
+
# the current user); the default scopes to a nested parent resource when one is present in the
|
|
846
|
+
# path, otherwise the model's default scope (all records).
|
|
847
|
+
def get_recordset
|
|
848
|
+
return nil unless self.class.model
|
|
851
849
|
|
|
852
|
-
|
|
853
|
-
cfg && cfg[:read_only]
|
|
854
|
-
end
|
|
850
|
+
self._rrf_nested_parent_recordset || self.class.model.all
|
|
855
851
|
end
|
|
856
852
|
|
|
857
|
-
#
|
|
858
|
-
|
|
859
|
-
|
|
853
|
+
# For a nested route, walk the whole parent chain in path order and return the innermost
|
|
854
|
+
# collection of this controller's model — e.g. `/movies/:movie_id/genres/:genre_id/tracks` becomes
|
|
855
|
+
# `Movie.find(movie_id).genres.find(genre_id).tracks`. Every link is enforced (a broken one raises
|
|
856
|
+
# `RecordNotFound` -> 404), and each association is resolved from its parent, so `belongs_to`,
|
|
857
|
+
# `has_many`, and `has_and_belongs_to_many` children all work. Each parent is looked up via its
|
|
858
|
+
# own controller's recordset (see `scope_nested_through_controllers`), so per-level access scoping
|
|
859
|
+
# is enforced. Returns `nil` when there is no nested parent, or a `<name>_id` param can't connect.
|
|
860
|
+
def _rrf_nested_parent_recordset
|
|
861
|
+
# Set on an ad-hoc parent instance below, so evaluating a parent's `get_recordset` doesn't
|
|
862
|
+
# recurse back into nested scoping (we want the parent's own scope, not to re-nest it).
|
|
863
|
+
return nil if @_rrf_scoping_parent
|
|
864
|
+
return nil unless self.class.scope_nested_by_parent && request
|
|
865
|
+
|
|
866
|
+
# `<name>_id` path parameters that name a model, in route order (outermost parent first).
|
|
867
|
+
parents = request.path_parameters.filter_map { |key, value|
|
|
868
|
+
key = key.to_s
|
|
869
|
+
next unless key.end_with?("_id")
|
|
870
|
+
|
|
871
|
+
model = key.delete_suffix("_id").classify.safe_constantize
|
|
872
|
+
next unless model.is_a?(Class) && model < ActiveRecord::Base
|
|
873
|
+
|
|
874
|
+
[ model, value ]
|
|
875
|
+
}
|
|
876
|
+
return nil if parents.empty?
|
|
877
|
+
|
|
878
|
+
# Find the outermost parent within its controller's scope, then descend: each next parent is
|
|
879
|
+
# constrained both to the previous parent's association and to its own controller's scope.
|
|
880
|
+
record = nil
|
|
881
|
+
parents.each do |model, id|
|
|
882
|
+
scope = _rrf_parent_recordset(model)
|
|
883
|
+
|
|
884
|
+
unless record.nil?
|
|
885
|
+
assoc = _rrf_collection_association_name(record.class, model)
|
|
886
|
+
return nil unless assoc
|
|
860
887
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
888
|
+
scope = record.public_send(assoc).merge(scope)
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
record = scope.find(id)
|
|
864
892
|
end
|
|
865
893
|
|
|
866
|
-
|
|
894
|
+
# Finally, the innermost parent's collection of this controller's model.
|
|
895
|
+
assoc = _rrf_collection_association_name(record.class, self.class.model)
|
|
896
|
+
return nil unless assoc
|
|
897
|
+
|
|
898
|
+
record.public_send(assoc)
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
# A parent's recordset for the nested-scope walk: its own controller's `get_recordset` (so that
|
|
902
|
+
# controller's access scoping is reused), or the bare model when the feature is off or no sibling
|
|
903
|
+
# controller is found. The ad-hoc instance shares this request and skips its own nested scoping.
|
|
904
|
+
def _rrf_parent_recordset(model)
|
|
905
|
+
return model.all unless self.class.scope_nested_through_controllers
|
|
906
|
+
|
|
907
|
+
controller = RESTFramework::Utils.controller_for_model(self.class, model)
|
|
908
|
+
return model.all unless controller
|
|
909
|
+
|
|
910
|
+
instance = controller.new
|
|
911
|
+
instance.request = request
|
|
912
|
+
instance.response = response
|
|
913
|
+
instance.instance_variable_set(:@_rrf_scoping_parent, true)
|
|
914
|
+
instance.get_recordset
|
|
915
|
+
end
|
|
916
|
+
|
|
917
|
+
# The name of `klass`'s collection association whose records are `target_model`, or `nil`.
|
|
918
|
+
def _rrf_collection_association_name(klass, target_model)
|
|
919
|
+
klass.reflect_on_all_associations.find { |ref|
|
|
920
|
+
ref.collection? && !ref.polymorphic? && ref.klass == target_model
|
|
921
|
+
}&.name
|
|
867
922
|
end
|
|
868
923
|
|
|
869
924
|
# Filter the recordset and return records this request has access to.
|
|
@@ -32,20 +32,12 @@ module ActionDispatch::Routing
|
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
# Route
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
raise ArgumentError, "rest_route: options and a block require a single name"
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
names.each { |name| _rrf_rest_route(name, **kwargs, &block) }
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Route a single controller from its action store.
|
|
48
|
-
def _rrf_rest_route(name, **kwargs)
|
|
35
|
+
# Route a single controller from its action store. Whether it routes as a singular `resource` or
|
|
36
|
+
# a plural `resources` is decided by the controller's own config (`singular`, and whether it has
|
|
37
|
+
# a `model`) — not by the method name: a plural model controller gets collection/member scopes,
|
|
38
|
+
# while singular and non-model controllers route everything at the root. Pass a block to nest
|
|
39
|
+
# resources like Rails' `resources` (the nested controller resolves in the current scope).
|
|
40
|
+
def rest_resource(name, **kwargs)
|
|
49
41
|
controller = kwargs.delete(:controller) || name
|
|
50
42
|
if controller.is_a?(Class)
|
|
51
43
|
controller_class = controller
|
|
@@ -84,5 +76,17 @@ module ActionDispatch::Routing
|
|
|
84
76
|
yield if block_given?
|
|
85
77
|
end
|
|
86
78
|
end
|
|
79
|
+
|
|
80
|
+
# Route several controllers that share the same options in one call — handy for condensing a
|
|
81
|
+
# namespace. Per-name options (`path:`, `as:`, `controller:`) and a block only make sense for a
|
|
82
|
+
# single controller, so they're rejected when several names are given. The `s` is about routing
|
|
83
|
+
# multiple controllers, not plural routes — a single plural controller is still `rest_resource`.
|
|
84
|
+
def rest_resources(*names, **kwargs, &block)
|
|
85
|
+
if names.size > 1 && (block || (kwargs.keys & [ :path, :as, :controller ]).any?)
|
|
86
|
+
raise ArgumentError, "rest_resources: per-name options and a block require a single name"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
names.each { |name| rest_resource(name, **kwargs, &block) }
|
|
90
|
+
end
|
|
87
91
|
end
|
|
88
92
|
end
|
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: 2.0.0.
|
|
4
|
+
version: 2.0.0.beta5
|
|
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: 2026-08-
|
|
11
|
+
date: 2026-08-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|