rest_framework 2.0.0.beta4 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b09674d62848416f77b39e23bc7b586e105786c7605320ad61761fe76ebc033
4
- data.tar.gz: d8242a8db125185c3b1d778f4b4f2697a174d0bf17f796329cc4a0dfa11be99e
3
+ metadata.gz: e483f8e7b422c2a45545f8c6e5d6313cc8a3df9dc286e76d4a6f09b9c717d9bf
4
+ data.tar.gz: 16d95dda8928c50b01cbc9cc291778928c9023d77e05c35f51cc9c8411a4c7d0
5
5
  SHA512:
6
- metadata.gz: 8399d7751f5116a6196853e64c704f153c33e0a79923fa6fa0c12a3867f0669f3f7651f6ea4528b5bd976a76ea7e2e2604a87fcb1c39116d0148c79e34d36d30
7
- data.tar.gz: f32a2db5595e8aee9dc069aa4fcaf6f96fffc35cb69b4ebc2d76589951a5dc1fdcdeae46631ad5af1506dbb365d07749461e12702ca8b36d7f1afa12e3922e83
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 `rest_route` to route any controller. It wraps Rails' `resource` / `resources` routers, picking
141
- `resources` for a plural model controller and `resource` otherwise, and automatically routes the
142
- controller's built-in and extra actions. A controller with a `model` gets the full CRUD set; a
143
- modelless controller is routed at its root (its `index`, which renders `index_content`).
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
- rest_route :api # `ApiController` serves the `/api` root.
150
+ rest_resource :api # `ApiController` serves the `/api` root.
148
151
 
149
152
  namespace :api do
150
- rest_route :movies
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
- - **Unified routing** — one `rest_route` (accepting several names) replaces `rest_resource` /
185
- `rest_resources` / `rest_root`; a modelless controller serves the API root from its
186
- `index_content`.
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 `rest_resource` / `rest_resources` / `rest_root` with `rest_route`.
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.beta4
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,
@@ -840,16 +841,84 @@ module RESTFramework::Controller
840
841
  alias_method :get_update_params, :get_body_params
841
842
  alias_method :get_destroy_params, :get_body_params
842
843
 
843
- # Get the set of records this controller has access to.
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).
844
847
  def get_recordset
845
- return self.class.recordset if self.class.recordset
848
+ return nil unless self.class.model
849
+
850
+ self._rrf_nested_parent_recordset || self.class.model.all
851
+ end
852
+
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
846
887
 
847
- # If there is a model, return that model's default scope (all records by default).
848
- if self.class.model
849
- return self.class.model.all
888
+ scope = record.public_send(assoc).merge(scope)
889
+ end
890
+
891
+ record = scope.find(id)
850
892
  end
851
893
 
852
- nil
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
853
922
  end
854
923
 
855
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 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"
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.beta4
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-02 00:00:00.000000000 Z
11
+ date: 2026-08-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails