From 9e2d2a372d4c8eb14d32774187a8e9251672eb03 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 19 Oct 2026 12:44:19 -0400 Subject: [PATCH 1/5] actionpack: type Resources#new as the block value In Rails 6.0 through 8.1, Mapper::Resources#new yields through with_scope_level and path_scope, each of which returns the block's value. Replace the generated untyped signature with a generic one in actiondispatch.rbs, and test it in the 7.2 and 8.1 suites. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01SEntZKsZwQHhzqcwG7MTqn --- gems/actionpack/6.0/actiondispatch.rbs | 2 ++ gems/actionpack/6.0/actionpack-generated.rbs | 2 -- gems/actionpack/7.2/_test/test.rb | 7 +++++++ gems/actionpack/7.2/actiondispatch.rbs | 2 ++ gems/actionpack/7.2/actionpack-generated.rbs | 2 -- gems/actionpack/8.1/_test/test.rb | 7 +++++++ gems/actionpack/8.1/actiondispatch.rbs | 2 ++ gems/actionpack/8.1/actionpack-generated.rbs | 2 -- 8 files changed, 20 insertions(+), 6 deletions(-) diff --git a/gems/actionpack/6.0/actiondispatch.rbs b/gems/actionpack/6.0/actiondispatch.rbs index 72e583220..be3c69451 100644 --- a/gems/actionpack/6.0/actiondispatch.rbs +++ b/gems/actionpack/6.0/actiondispatch.rbs @@ -279,6 +279,8 @@ module ActionDispatch # # resource actions are at /admin/posts. # resources :posts, path: "admin/posts" def resources: (*untyped resources) ?{ () -> untyped } -> untyped + + def new: [T] () { () -> T } -> T end end end diff --git a/gems/actionpack/6.0/actionpack-generated.rbs b/gems/actionpack/6.0/actionpack-generated.rbs index 4b072385c..013ff9f49 100644 --- a/gems/actionpack/6.0/actionpack-generated.rbs +++ b/gems/actionpack/6.0/actionpack-generated.rbs @@ -9303,8 +9303,6 @@ module ActionDispatch # preview_photo_url and preview_photo_path helpers. def member: () { () -> untyped } -> untyped - def new: () { () -> untyped } -> untyped - def nested: () { () -> untyped } -> untyped def shallow: () { () -> untyped } -> untyped diff --git a/gems/actionpack/7.2/_test/test.rb b/gems/actionpack/7.2/_test/test.rb index edda1d0db..87910b77f 100644 --- a/gems/actionpack/7.2/_test/test.rb +++ b/gems/actionpack/7.2/_test/test.rb @@ -31,3 +31,10 @@ def show request_http_digest_authentication("Admin", "Denied").length end end + +# Resources#new returns the block value +ActionDispatch::Routing::RouteSet.new.draw do + resources :photos do + new { 1 }.succ + end +end diff --git a/gems/actionpack/7.2/actiondispatch.rbs b/gems/actionpack/7.2/actiondispatch.rbs index f0c6a05e5..f0a983402 100644 --- a/gems/actionpack/7.2/actiondispatch.rbs +++ b/gems/actionpack/7.2/actiondispatch.rbs @@ -279,6 +279,8 @@ module ActionDispatch # # resource actions are at /admin/posts. # resources :posts, path: "admin/posts" def resources: (*untyped resources) ?{ () -> untyped } -> untyped + + def new: [T] () { () -> T } -> T end end end diff --git a/gems/actionpack/7.2/actionpack-generated.rbs b/gems/actionpack/7.2/actionpack-generated.rbs index 7136f58cb..61fbf461c 100644 --- a/gems/actionpack/7.2/actionpack-generated.rbs +++ b/gems/actionpack/7.2/actionpack-generated.rbs @@ -9278,8 +9278,6 @@ module ActionDispatch # preview_photo_url and preview_photo_path helpers. def member: () { () -> untyped } -> untyped - def new: () { () -> untyped } -> untyped - def nested: () { () -> untyped } -> untyped def shallow: () { () -> untyped } -> untyped diff --git a/gems/actionpack/8.1/_test/test.rb b/gems/actionpack/8.1/_test/test.rb index db5a44c68..37365eb18 100644 --- a/gems/actionpack/8.1/_test/test.rb +++ b/gems/actionpack/8.1/_test/test.rb @@ -117,3 +117,10 @@ def show request_http_digest_authentication("Admin", "Denied").length end end + +# Resources#new returns the block value +ActionDispatch::Routing::RouteSet.new.draw do + resources :photos do + new { 1 }.succ + end +end diff --git a/gems/actionpack/8.1/actiondispatch.rbs b/gems/actionpack/8.1/actiondispatch.rbs index 5edd8f208..79b74334c 100644 --- a/gems/actionpack/8.1/actiondispatch.rbs +++ b/gems/actionpack/8.1/actiondispatch.rbs @@ -275,6 +275,8 @@ module ActionDispatch # # resource actions are at /admin/posts. # resources :posts, path: "admin/posts" def resources: (*untyped resources) ?{ () -> untyped } -> untyped + + def new: [T] () { () -> T } -> T end end end diff --git a/gems/actionpack/8.1/actionpack-generated.rbs b/gems/actionpack/8.1/actionpack-generated.rbs index c1aa5d0e6..1550ca270 100644 --- a/gems/actionpack/8.1/actionpack-generated.rbs +++ b/gems/actionpack/8.1/actionpack-generated.rbs @@ -11105,8 +11105,6 @@ module ActionDispatch # `preview_photo_path` helpers. def member: () { (?) -> untyped } -> untyped - def new: () { (?) -> untyped } -> untyped - def nested: () { (?) -> untyped } -> untyped # See ActionDispatch::Routing::Mapper::Scoping#namespace. From 3f4e2011183c1f92d1eb7879ab79ed38d1377eac Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 19 Oct 2026 12:52:50 -0400 Subject: [PATCH 2/5] actionpack: leave 6.0 unchanged gems/actionpack/README.md accepts changes only for Rails versions under maintenance, and 6.0 is not one, so restore its RBS. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01SEntZKsZwQHhzqcwG7MTqn --- gems/actionpack/6.0/actiondispatch.rbs | 2 -- gems/actionpack/6.0/actionpack-generated.rbs | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gems/actionpack/6.0/actiondispatch.rbs b/gems/actionpack/6.0/actiondispatch.rbs index be3c69451..72e583220 100644 --- a/gems/actionpack/6.0/actiondispatch.rbs +++ b/gems/actionpack/6.0/actiondispatch.rbs @@ -279,8 +279,6 @@ module ActionDispatch # # resource actions are at /admin/posts. # resources :posts, path: "admin/posts" def resources: (*untyped resources) ?{ () -> untyped } -> untyped - - def new: [T] () { () -> T } -> T end end end diff --git a/gems/actionpack/6.0/actionpack-generated.rbs b/gems/actionpack/6.0/actionpack-generated.rbs index 013ff9f49..4b072385c 100644 --- a/gems/actionpack/6.0/actionpack-generated.rbs +++ b/gems/actionpack/6.0/actionpack-generated.rbs @@ -9303,6 +9303,8 @@ module ActionDispatch # preview_photo_url and preview_photo_path helpers. def member: () { () -> untyped } -> untyped + def new: () { () -> untyped } -> untyped + def nested: () { () -> untyped } -> untyped def shallow: () { () -> untyped } -> untyped From 6136e5544e85a660da33288bde2dc48c56472782 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 19 Oct 2026 13:02:41 -0400 Subject: [PATCH 3/5] actionpack: type the routing Mapper DSL return values In Rails 7.2 and 8.1 the Mapper DSL methods either return the mapper (resource, resources, nested, namespace, scope, constraints, the HTTP verb helpers, mount, with_default_scope, and match/root in 7.2) or the value of their block (collection, member, shallow, controller, defaults), because they end in `self` or in `yield` through with_scope_level, path_scope and shallow_scope. The generated signatures returned untyped and required blocks on match (7.2) and concern, which also accepts a callable. In 8.1, Resources#match returns its path_or_actions array (it ends in `path_or_actions.each`), and root returns match's value, so both are typed as Array. Concerns#concerns likewise ends in `each`. Move these declarations out of actionpack-generated.rbs into actiondispatch.rbs, add the missing 7.2 HttpHelpers#options, and cover them in the 7.2 and 8.1 route-drawing tests. Sources: actionpack/lib/action_dispatch/routing/mapper.rb at v7.2.4 and v8.1.4. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01SEntZKsZwQHhzqcwG7MTqn --- gems/actionpack/7.2/_test/test.rb | 26 +- gems/actionpack/7.2/actiondispatch.rbs | 56 +++- gems/actionpack/7.2/actionpack-generated.rbs | 293 ------------------- gems/actionpack/8.1/_test/test.rb | 25 +- gems/actionpack/8.1/actiondispatch.rbs | 52 +++- gems/actionpack/8.1/actionpack-generated.rbs | 276 ----------------- 6 files changed, 139 insertions(+), 589 deletions(-) diff --git a/gems/actionpack/7.2/_test/test.rb b/gems/actionpack/7.2/_test/test.rb index 87910b77f..f653e5d7b 100644 --- a/gems/actionpack/7.2/_test/test.rb +++ b/gems/actionpack/7.2/_test/test.rb @@ -32,9 +32,29 @@ def show end end -# Resources#new returns the block value +# Routing DSL methods return the mapper or their block value ActionDispatch::Routing::RouteSet.new.draw do - resources :photos do + resources(:photos) do + collection { get :search }.has_named_route?(:search_photos) + member { "preview" }.upcase new { 1 }.succ - end + shallow { [1] }.first + nested { resources :comments }.has_named_route?(:photo_comments) + namespace(:admin) { resource :stats }.has_named_route?(:admin_stats) + end.has_named_route?(:photos) + + scope(module: "admin") { get "dashboard", to: "dashboard#show" }.has_named_route?(:dashboard) + namespace(:api) { resources :posts }.has_named_route?(:api_posts) + constraints(subdomain: "api") { put "sync", to: "sync#update" }.has_named_route?(:sync) + controller(:pages) { 1 }.succ + defaults(format: :json) { "json" }.upcase + with_default_scope(module: "v1") { get "ping", to: "ping#show" }.has_named_route?(:ping) + + concern(:taggable) { |options| options.size } + concerns(:taggable).size + + get("status", to: "status#show").post("status", to: "status#create").has_named_route?(:status) + options("status", to: "status#options").has_named_route?(:status) + match("/hello", to: "hello#index", via: :get).has_named_route?(:hello) + root("home#index").has_named_route?(:root) end diff --git a/gems/actionpack/7.2/actiondispatch.rbs b/gems/actionpack/7.2/actiondispatch.rbs index f0a983402..493618cd5 100644 --- a/gems/actionpack/7.2/actiondispatch.rbs +++ b/gems/actionpack/7.2/actiondispatch.rbs @@ -86,31 +86,33 @@ module ActionDispatch # For supported arguments, see match[rdoc-ref:Base#match] # # get 'bacon', to: 'food#bacon' - def get: (*untyped args) ?{ () -> untyped } -> untyped + def get: (*untyped args) ?{ () -> untyped } -> self # Define a route that only recognizes HTTP POST. # For supported arguments, see match[rdoc-ref:Base#match] # # post 'bacon', to: 'food#bacon' - def post: (*untyped args) ?{ () -> untyped } -> untyped + def post: (*untyped args) ?{ () -> untyped } -> self # Define a route that only recognizes HTTP PATCH. # For supported arguments, see match[rdoc-ref:Base#match] # # patch 'bacon', to: 'food#bacon' - def patch: (*untyped args) ?{ () -> untyped } -> untyped + def patch: (*untyped args) ?{ () -> untyped } -> self # Define a route that only recognizes HTTP PUT. # For supported arguments, see match[rdoc-ref:Base#match] # # put 'bacon', to: 'food#bacon' - def put: (*untyped args) ?{ () -> untyped } -> untyped + def put: (*untyped args) ?{ () -> untyped } -> self # Define a route that only recognizes HTTP DELETE. # For supported arguments, see match[rdoc-ref:Base#match] # # delete 'broccoli', to: 'food#broccoli' - def delete: (*untyped args) ?{ () -> untyped } -> untyped + def delete: (*untyped args) ?{ () -> untyped } -> self + + def options: (*untyped args) ?{ () -> untyped } -> self private @@ -139,7 +141,7 @@ module ActionDispatch # # === Options # Takes same options as resources[rdoc-ref:#resources] - def resource: (*untyped resources) ?{ () -> untyped } -> untyped + def resource: (*untyped resources) ?{ () -> untyped } -> self # In Rails, a resourceful route provides a mapping between HTTP verbs # and URLs and controller actions. By convention, each action also maps @@ -278,9 +280,49 @@ module ActionDispatch # # # resource actions are at /admin/posts. # resources :posts, path: "admin/posts" - def resources: (*untyped resources) ?{ () -> untyped } -> untyped + def resources: (*untyped resources) ?{ () -> untyped } -> self def new: [T] () { () -> T } -> T + + def collection: [T] () { () -> T } -> T + + def member: [T] () { () -> T } -> T + + def nested: () { () -> void } -> self + + def namespace: (untyped path, ?::Hash[untyped, untyped] options) { () -> void } -> self + + def shallow: [T] () { () -> T } -> T + + def match: (untyped path, *untyped rest) ?{ () -> untyped } -> self + + def root: (untyped path, ?::Hash[untyped, untyped] options) -> self + end + + module Base + def mount: (untyped app, ?untyped? options) -> self + + def with_default_scope: (untyped scope) { () [self: ActionDispatch::Routing::Mapper] -> void } -> self + + def has_named_route?: (untyped name) -> bool + end + + module Scoping + def scope: (*untyped args) { () -> void } -> self + + def controller: [T] (untyped controller) { () -> T } -> T + + def namespace: (untyped path, ?::Hash[untyped, untyped] options) { () -> void } -> self + + def constraints: (?untyped constraints) { () -> void } -> self + + def defaults: [T] (?::Hash[untyped, untyped] defaults) { () -> T } -> T + end + + module Concerns + def concern: (untyped name, ?untyped? callable) ?{ (::Hash[untyped, untyped] options) [self: ActionDispatch::Routing::Mapper] -> void } -> untyped + + def concerns: (*untyped args) -> ::Array[untyped] end end end diff --git a/gems/actionpack/7.2/actionpack-generated.rbs b/gems/actionpack/7.2/actionpack-generated.rbs index 61fbf461c..7bb05ee03 100644 --- a/gems/actionpack/7.2/actionpack-generated.rbs +++ b/gems/actionpack/7.2/actionpack-generated.rbs @@ -8857,36 +8857,10 @@ module ActionDispatch # segment or disable it by supplying +false+. def match: (untyped path, ?untyped? options) -> nil - # Mount a Rack-based application to be used within the application. - # - # mount SomeRackApp, at: "some_route" - # - # Alternatively: - # - # mount(SomeRackApp => "some_route") - # - # For options, see +match+, as +mount+ uses it internally. - # - # All mounted applications come with routing helpers to access them. - # These are named after the class specified, so for the above example - # the helper is either +some_rack_app_path+ or +some_rack_app_url+. - # To customize this helper's name, use the +:as+ option: - # - # mount(SomeRackApp => "some_route", as: "exciting") - # - # This will generate the +exciting_path+ and +exciting_url+ helpers - # which can be used to navigate to this mounted app. - def mount: (untyped app, ?untyped? options) -> untyped - def default_url_options=: (untyped options) -> untyped alias default_url_options default_url_options= - def with_default_scope: (untyped scope) { () -> untyped } -> untyped - - # Query if the following named route was already defined. - def has_named_route?: (untyped name) -> untyped - private def rails_app?: (untyped app) -> untyped @@ -8951,151 +8925,8 @@ module ActionDispatch # PATCH/PUT /admin/posts/1 # DELETE /admin/posts/1 module Scoping - # Scopes a set of routes to the given default options. - # - # Take the following route definition as an example: - # - # scope path: ":account_id", as: "account" do - # resources :projects - # end - # - # This generates helpers such as +account_projects_path+, just like +resources+ does. - # The difference here being that the routes generated are like /:account_id/projects, - # rather than /accounts/:account_id/projects. - # - # === Options - # - # Takes same options as Base#match and Resources#resources. - # - # # route /posts (without the prefix /admin) to Admin::PostsController - # scope module: "admin" do - # resources :posts - # end - # - # # prefix the posts resource's requests with '/admin' - # scope path: "/admin" do - # resources :posts - # end - # - # # prefix the routing helper name: +sekret_posts_path+ instead of +posts_path+ - # scope as: "sekret" do - # resources :posts - # end - def scope: (*untyped args) { () -> untyped } -> untyped - POISON: untyped - # Scopes routes to a specific controller - # - # controller "food" do - # match "bacon", action: :bacon, via: :get - # end - def controller: (untyped controller) { () -> untyped } -> untyped - - # Scopes routes to a specific namespace. For example: - # - # namespace :admin do - # resources :posts - # end - # - # This generates the following routes: - # - # admin_posts GET /admin/posts(.:format) admin/posts#index - # admin_posts POST /admin/posts(.:format) admin/posts#create - # new_admin_post GET /admin/posts/new(.:format) admin/posts#new - # edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit - # admin_post GET /admin/posts/:id(.:format) admin/posts#show - # admin_post PATCH/PUT /admin/posts/:id(.:format) admin/posts#update - # admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy - # - # === Options - # - # The +:path+, +:as+, +:module+, +:shallow_path+ and +:shallow_prefix+ - # options all default to the name of the namespace. - # - # For options, see Base#match. For +:shallow_path+ option, see - # Resources#resources. - # - # # accessible through /sekret/posts rather than /admin/posts - # namespace :admin, path: "sekret" do - # resources :posts - # end - # - # # maps to Sekret::PostsController rather than Admin::PostsController - # namespace :admin, module: "sekret" do - # resources :posts - # end - # - # # generates +sekret_posts_path+ rather than +admin_posts_path+ - # namespace :admin, as: "sekret" do - # resources :posts - # end - def namespace: (untyped path, ?::Hash[untyped, untyped] options) { () -> untyped } -> untyped - - # === Parameter Restriction - # Allows you to constrain the nested routes based on a set of rules. - # For instance, in order to change the routes to allow for a dot character in the +id+ parameter: - # - # constraints(id: /\d+\.\d+/) do - # resources :posts - # end - # - # Now routes such as +/posts/1+ will no longer be valid, but +/posts/1.1+ will be. - # The +id+ parameter must match the constraint passed in for this example. - # - # You may use this to also restrict other parameters: - # - # resources :posts do - # constraints(post_id: /\d+\.\d+/) do - # resources :comments - # end - # end - # - # === Restricting based on IP - # - # Routes can also be constrained to an IP or a certain range of IP addresses: - # - # constraints(ip: /192\.168\.\d+\.\d+/) do - # resources :posts - # end - # - # Any user connecting from the 192.168.* range will be able to see this resource, - # where as any user connecting outside of this range will be told there is no such route. - # - # === Dynamic request matching - # - # Requests to routes can be constrained based on specific criteria: - # - # constraints(-> (req) { req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do - # resources :iphones - # end - # - # You are able to move this logic out into a class if it is too complex for routes. - # This class must have a +matches?+ method defined on it which either returns +true+ - # if the user should be given access to that route, or +false+ if the user should not. - # - # class Iphone - # def self.matches?(request) - # request.env["HTTP_USER_AGENT"] =~ /iPhone/ - # end - # end - # - # An expected place for this code would be +lib/constraints+. - # - # This class is then used like this: - # - # constraints(Iphone) do - # resources :iphones - # end - def constraints: (?::Hash[untyped, untyped] constraints) { () -> untyped } -> untyped - - # Allows you to set default parameters for a route, such as this: - # defaults id: 'home' do - # match 'scoped_pages/(:id)', to: 'pages#show' - # end - # Using this, the +:id+ parameter here will default to 'home'. - def defaults: (?::Hash[untyped, untyped] defaults) { () -> untyped } -> untyped - private def merge_path_scope: (untyped parent, untyped child) -> untyped @@ -9251,62 +9082,8 @@ module ActionDispatch def resources_path_names: (untyped options) -> untyped - # To add a route to the collection: - # - # resources :photos do - # collection do - # get 'search' - # end - # end - # - # This will enable Rails to recognize paths such as /photos/search - # with GET, and route to the search action of +PhotosController+. It will also - # create the search_photos_url and search_photos_path - # route helpers. - def collection: () { () -> untyped } -> untyped - - # To add a member route, add a member block into the resource block: - # - # resources :photos do - # member do - # get 'preview' - # end - # end - # - # This will recognize /photos/1/preview with GET, and route to the - # preview action of +PhotosController+. It will also create the - # preview_photo_url and preview_photo_path helpers. - def member: () { () -> untyped } -> untyped - - def nested: () { () -> untyped } -> untyped - - def shallow: () { () -> untyped } -> untyped - def shallow?: () -> untyped - # Matches a URL pattern to one or more routes. - # For more information, see match[rdoc-ref:Base#match]. - # - # match 'path' => 'controller#action', via: :patch - # match 'path', to: 'controller#action', via: :post - # match 'path', 'otherpath', on: :member, via: :get - def match: (untyped path, *untyped rest) { () -> untyped } -> untyped - - # You can specify what Rails should route "/" to with the root method: - # - # root to: 'pages#main' - # - # For options, see +match+, as +root+ uses it internally. - # - # You can also pass a string which will expand - # - # root 'pages#main' - # - # You should put the root route at the top of config/routes.rb, - # because this means it will be matched first. As this is the most popular route - # of most Rails applications, this is beneficial. - def root: (untyped path, ?::Hash[untyped, untyped] options) -> untyped - private def parent_resource: () -> untyped @@ -9389,76 +9166,6 @@ module ActionDispatch # concerns :commentable # end module Concerns - # Define a routing concern using a name. - # - # Concerns may be defined inline, using a block, or handled by - # another object, by passing that object as the second parameter. - # - # The concern object, if supplied, should respond to call, - # which will receive two parameters: - # - # * The current mapper - # * A hash of options which the concern object may use - # - # Options may also be used by concerns defined in a block by accepting - # a block parameter. So, using a block, you might do something as - # simple as limit the actions available on certain resources, passing - # standard resource options through the concern: - # - # concern :commentable do |options| - # resources :comments, options - # end - # - # resources :posts, concerns: :commentable - # resources :archived_posts do - # # Don't allow comments on archived posts - # concerns :commentable, only: [:index, :show] - # end - # - # Or, using a callable object, you might implement something more - # specific to your application, which would be out of place in your - # routes file. - # - # # purchasable.rb - # class Purchasable - # def initialize(defaults = {}) - # @defaults = defaults - # end - # - # def call(mapper, options = {}) - # options = @defaults.merge(options) - # mapper.resources :purchases - # mapper.resources :receipts - # mapper.resources :returns if options[:returnable] - # end - # end - # - # # routes.rb - # concern :purchasable, Purchasable.new(returnable: true) - # - # resources :toys, concerns: :purchasable - # resources :electronics, concerns: :purchasable - # resources :pets do - # concerns :purchasable, returnable: false - # end - # - # Any routing helpers can be used inside a concern. If using a - # callable, they're accessible from the Mapper that's passed to - # call. - def concern: (untyped name, ?untyped? callable) { () -> untyped } -> untyped - - # Use the named concerns - # - # resources :posts do - # concerns :commentable - # end - # - # Concerns also work in any routes helper that you want to use: - # - # namespace :posts do - # concerns :commentable - # end - def concerns: (*untyped args) -> untyped end module CustomUrls diff --git a/gems/actionpack/8.1/_test/test.rb b/gems/actionpack/8.1/_test/test.rb index 37365eb18..5794a7a5c 100644 --- a/gems/actionpack/8.1/_test/test.rb +++ b/gems/actionpack/8.1/_test/test.rb @@ -118,9 +118,28 @@ def show end end -# Resources#new returns the block value +# Routing DSL methods return the mapper or their block value ActionDispatch::Routing::RouteSet.new.draw do - resources :photos do + resources(:photos) do + collection { get :search }.has_named_route?(:search_photos) + member { "preview" }.upcase new { 1 }.succ - end + shallow { [1] }.first + nested { resources :comments }.has_named_route?(:photo_comments) + namespace(:admin) { resource :stats }.has_named_route?(:admin_stats) + end.has_named_route?(:photos) + + scope(module: "admin") { get "dashboard", to: "dashboard#show" }.has_named_route?(:dashboard) + namespace(:api) { resources :posts }.has_named_route?(:api_posts) + constraints(subdomain: "api") { put "sync", to: "sync#update" }.has_named_route?(:sync) + controller(:pages) { 1 }.succ + defaults(format: :json) { "json" }.upcase + with_default_scope(module: "v1") { get "ping", to: "ping#show" }.has_named_route?(:ping) + + concern(:taggable) { |options| options.size } + concerns(:taggable).size + + get("status", to: "status#show").post("status", to: "status#create").has_named_route?(:status) + match("/hello", to: "hello#index", via: :get).size + root("home#index").size end diff --git a/gems/actionpack/8.1/actiondispatch.rbs b/gems/actionpack/8.1/actiondispatch.rbs index 79b74334c..d130b672d 100644 --- a/gems/actionpack/8.1/actiondispatch.rbs +++ b/gems/actionpack/8.1/actiondispatch.rbs @@ -86,31 +86,31 @@ module ActionDispatch # For supported arguments, see match[rdoc-ref:Base#match] # # get 'bacon', to: 'food#bacon' - def get: (*untyped args) ?{ () -> untyped } -> untyped + def get: (*untyped args) ?{ () -> untyped } -> self # Define a route that only recognizes HTTP POST. # For supported arguments, see match[rdoc-ref:Base#match] # # post 'bacon', to: 'food#bacon' - def post: (*untyped args) ?{ () -> untyped } -> untyped + def post: (*untyped args) ?{ () -> untyped } -> self # Define a route that only recognizes HTTP PATCH. # For supported arguments, see match[rdoc-ref:Base#match] # # patch 'bacon', to: 'food#bacon' - def patch: (*untyped args) ?{ () -> untyped } -> untyped + def patch: (*untyped args) ?{ () -> untyped } -> self # Define a route that only recognizes HTTP PUT. # For supported arguments, see match[rdoc-ref:Base#match] # # put 'bacon', to: 'food#bacon' - def put: (*untyped args) ?{ () -> untyped } -> untyped + def put: (*untyped args) ?{ () -> untyped } -> self # Define a route that only recognizes HTTP DELETE. # For supported arguments, see match[rdoc-ref:Base#match] # # delete 'broccoli', to: 'food#broccoli' - def delete: (*untyped args) ?{ () -> untyped } -> untyped + def delete: (*untyped args) ?{ () -> untyped } -> self end module Resources @@ -135,7 +135,7 @@ module ActionDispatch # # === Options # Takes same options as resources[rdoc-ref:#resources] - def resource: (*untyped resources) ?{ () -> untyped } -> untyped + def resource: (*untyped resources) ?{ () -> untyped } -> self # In Rails, a resourceful route provides a mapping between HTTP verbs # and URLs and controller actions. By convention, each action also maps @@ -274,9 +274,47 @@ module ActionDispatch # # # resource actions are at /admin/posts. # resources :posts, path: "admin/posts" - def resources: (*untyped resources) ?{ () -> untyped } -> untyped + def resources: (*untyped resources) ?{ () -> untyped } -> self def new: [T] () { () -> T } -> T + + def collection: [T] () { () -> T } -> T + + def member: [T] () { () -> T } -> T + + def nested: () { () -> void } -> self + + def namespace: (untyped name, ?untyped? deprecated_options, ?as: untyped, ?path: untyped, ?shallow_path: untyped, ?shallow_prefix: untyped, **untyped options) { () -> void } -> self + + def shallow: [T] () { () -> T } -> T + + def match: (*untyped path_or_actions, ?as: untyped, ?via: untyped?, ?to: untyped?, ?controller: untyped?, ?action: untyped?, ?on: untyped?, ?defaults: untyped?, ?constraints: untyped?, ?anchor: untyped?, ?format: untyped?, ?path: untyped?, ?internal: untyped?, **untyped mapping) ?{ (?) -> untyped } -> ::Array[untyped] + + def root: (untyped path, ?::Hash[untyped, untyped] options) -> ::Array[untyped] + end + + module Base + def with_default_scope: (untyped scope) { () [self: ActionDispatch::Routing::Mapper] -> void } -> self + + def has_named_route?: (untyped name) -> bool + end + + module Scoping + def scope: (*untyped args, ?only: untyped?, ?except: untyped?, **untyped options) { () -> void } -> self + + def controller: [T] (untyped controller) { () -> T } -> T + + def namespace: (untyped name, ?untyped? deprecated_options, ?as: untyped, ?path: untyped, ?shallow_path: untyped, ?shallow_prefix: untyped, **untyped options) { () -> void } -> self + + def constraints: (?untyped constraints) { () -> void } -> self + + def defaults: [T] (?::Hash[untyped, untyped] defaults) { () -> T } -> T + end + + module Concerns + def concern: (untyped name, ?untyped? callable) ?{ (::Hash[Symbol, untyped] options) [self: ActionDispatch::Routing::Mapper] -> void } -> untyped + + def concerns: (*untyped args, **untyped options) -> ::Array[untyped] end end end diff --git a/gems/actionpack/8.1/actionpack-generated.rbs b/gems/actionpack/8.1/actionpack-generated.rbs index 1550ca270..f798926f8 100644 --- a/gems/actionpack/8.1/actionpack-generated.rbs +++ b/gems/actionpack/8.1/actionpack-generated.rbs @@ -10684,11 +10684,6 @@ module ActionDispatch alias default_url_options default_url_options= - def with_default_scope: (untyped scope) { (?) -> untyped } -> untyped - - # Query if the following named route was already defined. - def has_named_route?: (untyped name) -> untyped - private def assign_deprecated_option: (untyped deprecated_options, untyped key, untyped method_name) -> (untyped | nil) @@ -10772,156 +10767,8 @@ module ActionDispatch # PATCH/PUT /admin/posts/1 # DELETE /admin/posts/1 module Scoping - # Scopes a set of routes to the given default options. - # - # Take the following route definition as an example: - # - # scope path: ":account_id", as: "account" do - # resources :projects - # end - # - # This generates helpers such as `account_projects_path`, just like `resources` - # does. The difference here being that the routes generated are like - # /:account_id/projects, rather than /accounts/:account_id/projects. - # - # ### Options - # - # Takes same options as `Base#match` and `Resources#resources`. - # - # # route /posts (without the prefix /admin) to Admin::PostsController - # scope module: "admin" do - # resources :posts - # end - # - # # prefix the posts resource's requests with '/admin' - # scope path: "/admin" do - # resources :posts - # end - # - # # prefix the routing helper name: sekret_posts_path instead of posts_path - # scope as: "sekret" do - # resources :posts - # end - def scope: (*untyped args, ?only: untyped?, ?except: untyped?, **untyped options) { () -> untyped } -> untyped - POISON: untyped - # Scopes routes to a specific controller - # - # controller "food" do - # match "bacon", action: :bacon, via: :get - # end - def controller: (untyped controller) { () -> untyped } -> untyped - - # Scopes routes to a specific namespace. For example: - # - # namespace :admin do - # resources :posts - # end - # - # This generates the following routes: - # - # admin_posts GET /admin/posts(.:format) admin/posts#index - # admin_posts POST /admin/posts(.:format) admin/posts#create - # new_admin_post GET /admin/posts/new(.:format) admin/posts#new - # edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit - # admin_post GET /admin/posts/:id(.:format) admin/posts#show - # admin_post PATCH/PUT /admin/posts/:id(.:format) admin/posts#update - # admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy - # - # ### Options - # - # The `:path`, `:as`, `:module`, `:shallow_path`, and `:shallow_prefix` options - # all default to the name of the namespace. - # - # For options, see `Base#match`. For `:shallow_path` option, see - # `Resources#resources`. - # - # # accessible through /sekret/posts rather than /admin/posts - # namespace :admin, path: "sekret" do - # resources :posts - # end - # - # # maps to Sekret::PostsController rather than Admin::PostsController - # namespace :admin, module: "sekret" do - # resources :posts - # end - # - # # generates sekret_posts_path rather than admin_posts_path - # namespace :admin, as: "sekret" do - # resources :posts - # end - def namespace: (untyped name, ?untyped? deprecated_options, ?as: untyped, ?path: untyped, ?shallow_path: untyped, ?shallow_prefix: untyped, **untyped options) { (?) -> untyped } -> untyped - - # ### Parameter Restriction - # Allows you to constrain the nested routes based on a set of rules. For - # instance, in order to change the routes to allow for a dot character in the - # `id` parameter: - # - # constraints(id: /\d+\.\d+/) do - # resources :posts - # end - # - # Now routes such as `/posts/1` will no longer be valid, but `/posts/1.1` will - # be. The `id` parameter must match the constraint passed in for this example. - # - # You may use this to also restrict other parameters: - # - # resources :posts do - # constraints(post_id: /\d+\.\d+/) do - # resources :comments - # end - # end - # - # ### Restricting based on IP - # - # Routes can also be constrained to an IP or a certain range of IP addresses: - # - # constraints(ip: /192\.168\.\d+\.\d+/) do - # resources :posts - # end - # - # Any user connecting from the 192.168.* range will be able to see this - # resource, where as any user connecting outside of this range will be told - # there is no such route. - # - # ### Dynamic request matching - # - # Requests to routes can be constrained based on specific criteria: - # - # constraints(-> (req) { /iPhone/.match?(req.env["HTTP_USER_AGENT"]) }) do - # resources :iphones - # end - # - # You are able to move this logic out into a class if it is too complex for - # routes. This class must have a `matches?` method defined on it which either - # returns `true` if the user should be given access to that route, or `false` if - # the user should not. - # - # class Iphone - # def self.matches?(request) - # /iPhone/.match?(request.env["HTTP_USER_AGENT"]) - # end - # end - # - # An expected place for this code would be `lib/constraints`. - # - # This class is then used like this: - # - # constraints(Iphone) do - # resources :iphones - # end - def constraints: (?::Hash[untyped, untyped] constraints) { (?) -> untyped } -> untyped - - # Allows you to set default parameters for a route, such as this: - # - # defaults id: 'home' do - # match 'scoped_pages/(:id)', to: 'pages#show' - # end - # - # Using this, the `:id` parameter here will default to 'home'. - def defaults: (?::Hash[untyped, untyped] defaults) { () -> untyped } -> untyped - private def merge_path_scope: (untyped parent, untyped child) -> untyped @@ -11079,39 +10926,6 @@ module ActionDispatch def resources_path_names: (untyped options) -> untyped - # To add a route to the collection: - # - # resources :photos do - # collection do - # get 'search' - # end - # end - # - # This will enable Rails to recognize paths such as `/photos/search` with GET, - # and route to the search action of `PhotosController`. It will also create the - # `search_photos_url` and `search_photos_path` route helpers. - def collection: () { (?) -> untyped } -> untyped - - # To add a member route, add a member block into the resource block: - # - # resources :photos do - # member do - # get 'preview' - # end - # end - # - # This will recognize `/photos/1/preview` with GET, and route to the preview - # action of `PhotosController`. It will also create the `preview_photo_url` and - # `preview_photo_path` helpers. - def member: () { (?) -> untyped } -> untyped - - def nested: () { (?) -> untyped } -> untyped - - # See ActionDispatch::Routing::Mapper::Scoping#namespace. - def namespace: (untyped name, ?untyped? deprecated_options, ?as: untyped, ?path: untyped, ?shallow_path: untyped, ?shallow_prefix: untyped, **untyped options) { (?) -> untyped } -> untyped - - def shallow: () { () -> untyped } -> untyped - def shallow?: () -> untyped # Loads another routes file with the given `name` located inside the @@ -11138,28 +10952,6 @@ module ActionDispatch # single routes file. def draw: (untyped name) -> untyped - # Matches a URL pattern to one or more routes. For more information, see - # [match](rdoc-ref:Base#match). - # - # match 'path', to: 'controller#action', via: :post - # match 'otherpath', on: :member, via: :get - def match: (*untyped path_or_actions, ?as: untyped, ?via: untyped?, ?to: untyped?, ?controller: untyped?, ?action: untyped?, ?on: untyped?, ?defaults: untyped?, ?constraints: untyped?, ?anchor: untyped?, ?format: untyped?, ?path: untyped?, ?internal: untyped?, **untyped mapping) ?{ (?) -> untyped } -> untyped - - # You can specify what Rails should route "/" to with the root method: - # - # root to: 'pages#main' - # - # For options, see `match`, as `root` uses it internally. - # - # You can also pass a string which will expand - # - # root 'pages#main' - # - # You should put the root route at the top of `config/routes.rb`, because this - # means it will be matched first. As this is the most popular route of most - # Rails applications, this is beneficial. - def root: (untyped path, ?::Hash[untyped, untyped] options) -> untyped - private def parent_resource: () -> untyped @@ -11244,74 +11036,6 @@ module ActionDispatch # concerns :commentable # end module Concerns - # Define a routing concern using a name. - # - # Concerns may be defined inline, using a block, or handled by another object, - # by passing that object as the second parameter. - # - # The concern object, if supplied, should respond to `call`, which will receive - # two parameters: - # - # * The current mapper - # * A hash of options which the concern object may use - # - # Options may also be used by concerns defined in a block by accepting a block - # parameter. So, using a block, you might do something as simple as limit the - # actions available on certain resources, passing standard resource options - # through the concern: - # - # concern :commentable do |options| - # resources :comments, options - # end - # - # resources :posts, concerns: :commentable - # resources :archived_posts do - # # Don't allow comments on archived posts - # concerns :commentable, only: [:index, :show] - # end - # - # Or, using a callable object, you might implement something more specific to - # your application, which would be out of place in your routes file. - # - # # purchasable.rb - # class Purchasable - # def initialize(defaults = {}) - # @defaults = defaults - # end - # - # def call(mapper, options = {}) - # options = @defaults.merge(options) - # mapper.resources :purchases - # mapper.resources :receipts - # mapper.resources :returns if options[:returnable] - # end - # end - # - # # routes.rb - # concern :purchasable, Purchasable.new(returnable: true) - # - # resources :toys, concerns: :purchasable - # resources :electronics, concerns: :purchasable - # resources :pets do - # concerns :purchasable, returnable: false - # end - # - # Any routing helpers can be used inside a concern. If using a callable, they're - # accessible from the Mapper that's passed to `call`. - def concern: (untyped name, ?untyped? callable) ?{ (?) -> untyped } -> untyped - - # Use the named concerns - # - # resources :posts do - # concerns :commentable - # end - # - # Concerns also work in any routes helper that you want to use: - # - # namespace :posts do - # concerns :commentable - # end - def concerns: (*untyped args, **untyped options) -> untyped end module CustomUrls From dd61ced08135e26605a6d7bd989ff387c5c32a75 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 19 Oct 2026 14:08:23 -0400 Subject: [PATCH 4/5] actionpack: type Mapper DSL arguments, keep docs The previous commit dropped the Rails doc comments of the declarations it moved out of actionpack-generated.rbs and typed only return values. Move each doc comment onto its hand-written declaration, and type the arguments, options and blocks from mapper.rb at v7.2.4 and v8.1.4: - Type aliases and interfaces in Mapper name the option values that Mapping, Resource and Scope accept: paths, `via`, `format`, `only`/`except`, `to` endpoints (strings, Rack apps and lambdas, controller classes), constraints (hashes, `matches?` objects and request lambdas), and `on`. - `get`..`connect`, `match` and `mount` gain an overload for the `"path" => "controller#action"` and `App => "path"` hash forms, and drop the block Rails never calls. - `concern` gets overloads for a lambda, a callable object, or a block run on the mapper. - 8.1 also moves mount, options and connect out of the generated file. In 7.2 the verbs and match take several paths and match/root return the mapper; in 8.1 they take one path and match/root return the path array. The tests draw routes in the forms the Rails routing guide uses. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01SEntZKsZwQHhzqcwG7MTqn --- gems/actionpack/7.2/_test/test.rb | 68 ++-- gems/actionpack/7.2/_test/test.rbs | 4 + gems/actionpack/7.2/actiondispatch.rbs | 384 ++++++++++++++++-- gems/actionpack/8.1/_test/test.rb | 69 ++-- gems/actionpack/8.1/_test/test.rbs | 4 + gems/actionpack/8.1/actiondispatch.rbs | 393 +++++++++++++++++-- gems/actionpack/8.1/actionpack-generated.rbs | 30 -- 7 files changed, 803 insertions(+), 149 deletions(-) diff --git a/gems/actionpack/7.2/_test/test.rb b/gems/actionpack/7.2/_test/test.rb index f653e5d7b..377cc99fc 100644 --- a/gems/actionpack/7.2/_test/test.rb +++ b/gems/actionpack/7.2/_test/test.rb @@ -32,29 +32,53 @@ def show end end -# Routing DSL methods return the mapper or their block value +# Routing DSL forms from the Rails routing guide +class AdminConstraint + def matches?(request) + request.path.start_with?("/admin") + end +end + ActionDispatch::Routing::RouteSet.new.draw do - resources(:photos) do - collection { get :search }.has_named_route?(:search_photos) - member { "preview" }.upcase - new { 1 }.succ - shallow { [1] }.first - nested { resources :comments }.has_named_route?(:photo_comments) - namespace(:admin) { resource :stats }.has_named_route?(:admin_stats) - end.has_named_route?(:photos) - - scope(module: "admin") { get "dashboard", to: "dashboard#show" }.has_named_route?(:dashboard) - namespace(:api) { resources :posts }.has_named_route?(:api_posts) - constraints(subdomain: "api") { put "sync", to: "sync#update" }.has_named_route?(:sync) - controller(:pages) { 1 }.succ - defaults(format: :json) { "json" }.upcase - with_default_scope(module: "v1") { get "ping", to: "ping#show" }.has_named_route?(:ping) + root "pages#home" + root(to: "pages#home", as: :home).has_named_route?(:home) + + get "profile", to: "users#show", as: :profile + get "photos/search" => "photos#search" + get "health", to: ->(_env) { [200, { "content-type" => "text/plain" }, ["OK"]] } + get "old", to: redirect("/new") + post "uploads", controller: :uploads, action: :create, defaults: { format: "json" }, format: false + match("photos", "pictures", to: "photos#show", via: [:get, :post]).has_named_route?(:photos) + match "exports/:id", to: "exports#show", via: :all, constraints: { id: /\d+/ }, anchor: true - concern(:taggable) { |options| options.size } - concerns(:taggable).size + resources :photos, only: [:index, :show], param: :slug, path: "pictures", as: :images do + member { get :preview }.has_named_route?(:preview_image) + collection { get :search }.has_named_route?(:search_images) + new { get :draft }.has_named_route?(:draft_new_image) + get :recent, on: :collection + nested { resources :comments, except: :destroy }.has_named_route?(:image_comments) + end.has_named_route?(:images) + resource :geocoder, controller: "geocoders", module: "maps" + resources :articles, concerns: [:commentable], path_names: { new: "make" }, shallow: true + shallow { resources(:posts) { resources :drafts } }.has_named_route?(:posts) - get("status", to: "status#show").post("status", to: "status#create").has_named_route?(:status) - options("status", to: "status#options").has_named_route?(:status) - match("/hello", to: "hello#index", via: :get).has_named_route?(:hello) - root("home#index").has_named_route?(:root) + namespace(:admin, path: "control", as: :ctl) { resources :articles }.has_named_route?(:ctl_articles) + scope(module: "admin", path: "manage", as: :manage) { resources :users }.has_named_route?(:manage_users) + scope("(:locale)", locale: /en|nl/) { resources :books } + scope(only: :index) { resources :tags } + + controller(:pages) { get "about", action: :about }.has_named_route?(:about) + defaults(format: :json) { get "status", to: "status#show" }.has_named_route?(:status) + + constraints(subdomain: "api") { resources :keys } + constraints(id: /\d+/) { get "items/:id", to: "items#show" } + constraints(->(request) { request.xhr? }) { get "async", to: "async#show" } + constraints(AdminConstraint.new) { get "admin", to: "admin#index" } + get "secure", to: "secure#show", constraints: ->(request) { request.ssl? } + + concern(:commentable) { |options| resources :comments, **options } + concerns(:commentable, only: :index).first&.to_proc + + mount Rack::Head.new(->(_env) { [200, { "content-type" => "text/plain" }, ["OK"]] }), at: "/head", as: :head + with_default_scope(module: "v1") { get "ping", to: "ping#show" }.has_named_route?(:ping) end diff --git a/gems/actionpack/7.2/_test/test.rbs b/gems/actionpack/7.2/_test/test.rbs index 18bcbc7ce..b1a08e958 100644 --- a/gems/actionpack/7.2/_test/test.rbs +++ b/gems/actionpack/7.2/_test/test.rbs @@ -10,3 +10,7 @@ end class AuthController < ActionController::Base def show: () -> void end + +class AdminConstraint + def matches?: (ActionDispatch::Request request) -> bool +end diff --git a/gems/actionpack/7.2/actiondispatch.rbs b/gems/actionpack/7.2/actiondispatch.rbs index 493618cd5..8c0d69547 100644 --- a/gems/actionpack/7.2/actiondispatch.rbs +++ b/gems/actionpack/7.2/actiondispatch.rbs @@ -81,44 +81,332 @@ module ActionDispatch end class Mapper + # A route path, resource name, controller, or action. + type path = String | Symbol + + # HTTP verbs the route recognizes, such as `:get`, `[:get, :post]`, or `:all`. + type via = Symbol | Array[Symbol] + + # `true` requires a format segment, `false` drops it, and a String or Regexp constrains it. + type format = bool | String | Regexp | nil + + # Values accepted by the `:only` and `:except` resource options. + type actions = path | Array[path] + + # Values accepted by the `:to` option: `"controller#action"`, an action name, a Rack application, or a controller class. + type endpoint = String | ^(Rack::env env) -> Rack::response | Rack::_Application | singleton(ActionController::Metal) + + # Request constraints are objects responding to `matches?`, or callables taking the request. + interface _RequestMatcher + def matches?: (ActionDispatch::Request request) -> boolish + end + + type request_constraint = ^(ActionDispatch::Request request) -> boolish | _RequestMatcher + + # Segment constraints (`{ id: /\d+/ }`) and request conditions (`{ subdomain: "api" }`), or a request constraint. + type constraints = Hash[Symbol, untyped] | request_constraint + + # The scope a member, collection, or new route is added to. + type on = :member | :collection | :new + + # A concern defined with a callable receives the mapper and the options given to `concerns`. + interface _Concern + def call: (ActionDispatch::Routing::Mapper mapper, Hash[Symbol, untyped] options) -> void + end + + module Base + # Mount a Rack-based application to be used within the application. + # + # mount SomeRackApp, at: "some_route" + # + # Alternatively: + # + # mount(SomeRackApp => "some_route") + # + # For options, see +match+, as +mount+ uses it internally. + # + # All mounted applications come with routing helpers to access them. + # These are named after the class specified, so for the above example + # the helper is either +some_rack_app_path+ or +some_rack_app_url+. + # To customize this helper's name, use the +:as+ option: + # + # mount(SomeRackApp => "some_route", as: "exciting") + # + # This will generate the +exciting_path+ and +exciting_url+ helpers + # which can be used to navigate to this mounted app. + def mount: (untyped app, at: String, ?as: path?, ?via: via, ?defaults: Hash[Symbol, untyped], ?constraints: constraints, ?anchor: bool, ?format: format, ?path: path, ?internal: bool, **untyped) -> self + | (Hash[untyped, untyped] app_to_path_and_options) -> self + + def with_default_scope: (Hash[Symbol, untyped] scope) { () [self: ActionDispatch::Routing::Mapper] -> void } -> self + + # Query if the following named route was already defined. + def has_named_route?: (path name) -> bool + end + module HttpHelpers # Define a route that only recognizes HTTP GET. # For supported arguments, see match[rdoc-ref:Base#match] # # get 'bacon', to: 'food#bacon' - def get: (*untyped args) ?{ () -> untyped } -> self + def get: (path path, *path paths, ?as: path?, ?to: endpoint, ?controller: path, ?action: path, ?on: on, ?defaults: Hash[Symbol, untyped], ?constraints: constraints, ?anchor: bool, ?format: format, ?path: path, ?internal: bool, **untyped) -> self + | (Hash[path, untyped] path_to_endpoint_and_options) -> self # Define a route that only recognizes HTTP POST. # For supported arguments, see match[rdoc-ref:Base#match] # # post 'bacon', to: 'food#bacon' - def post: (*untyped args) ?{ () -> untyped } -> self + def post: (path path, *path paths, ?as: path?, ?to: endpoint, ?controller: path, ?action: path, ?on: on, ?defaults: Hash[Symbol, untyped], ?constraints: constraints, ?anchor: bool, ?format: format, ?path: path, ?internal: bool, **untyped) -> self + | (Hash[path, untyped] path_to_endpoint_and_options) -> self # Define a route that only recognizes HTTP PATCH. # For supported arguments, see match[rdoc-ref:Base#match] # # patch 'bacon', to: 'food#bacon' - def patch: (*untyped args) ?{ () -> untyped } -> self + def patch: (path path, *path paths, ?as: path?, ?to: endpoint, ?controller: path, ?action: path, ?on: on, ?defaults: Hash[Symbol, untyped], ?constraints: constraints, ?anchor: bool, ?format: format, ?path: path, ?internal: bool, **untyped) -> self + | (Hash[path, untyped] path_to_endpoint_and_options) -> self # Define a route that only recognizes HTTP PUT. # For supported arguments, see match[rdoc-ref:Base#match] # # put 'bacon', to: 'food#bacon' - def put: (*untyped args) ?{ () -> untyped } -> self + def put: (path path, *path paths, ?as: path?, ?to: endpoint, ?controller: path, ?action: path, ?on: on, ?defaults: Hash[Symbol, untyped], ?constraints: constraints, ?anchor: bool, ?format: format, ?path: path, ?internal: bool, **untyped) -> self + | (Hash[path, untyped] path_to_endpoint_and_options) -> self # Define a route that only recognizes HTTP DELETE. # For supported arguments, see match[rdoc-ref:Base#match] # # delete 'broccoli', to: 'food#broccoli' - def delete: (*untyped args) ?{ () -> untyped } -> self + def delete: (path path, *path paths, ?as: path?, ?to: endpoint, ?controller: path, ?action: path, ?on: on, ?defaults: Hash[Symbol, untyped], ?constraints: constraints, ?anchor: bool, ?format: format, ?path: path, ?internal: bool, **untyped) -> self + | (Hash[path, untyped] path_to_endpoint_and_options) -> self - def options: (*untyped args) ?{ () -> untyped } -> self + def options: (path path, *path paths, ?as: path?, ?to: endpoint, ?controller: path, ?action: path, ?on: on, ?defaults: Hash[Symbol, untyped], ?constraints: constraints, ?anchor: bool, ?format: format, ?path: path, ?internal: bool, **untyped) -> self + | (Hash[path, untyped] path_to_endpoint_and_options) -> self private def map_method: (untyped method, untyped args) ?{ () -> untyped } -> untyped end + module Scoping + # Scopes a set of routes to the given default options. + # + # Take the following route definition as an example: + # + # scope path: ":account_id", as: "account" do + # resources :projects + # end + # + # This generates helpers such as +account_projects_path+, just like +resources+ does. + # The difference here being that the routes generated are like /:account_id/projects, + # rather than /accounts/:account_id/projects. + # + # === Options + # + # Takes same options as Base#match and Resources#resources. + # + # # route /posts (without the prefix /admin) to Admin::PostsController + # scope module: "admin" do + # resources :posts + # end + # + # # prefix the posts resource's requests with '/admin' + # scope path: "/admin" do + # resources :posts + # end + # + # # prefix the routing helper name: +sekret_posts_path+ instead of +posts_path+ + # scope as: "sekret" do + # resources :posts + # end + def scope: (*(path | Array[path]) paths, ?only: actions, ?except: actions, ?module: path, ?path: path, ?as: path, ?controller: path, ?action: path, ?constraints: constraints, ?defaults: Hash[Symbol, untyped], ?shallow: bool, ?shallow_path: path, ?shallow_prefix: path, ?path_names: Hash[Symbol, String], ?format: format, ?via: via, ?to: endpoint, **untyped) { () -> void } -> self + + # Scopes routes to a specific controller + # + # controller "food" do + # match "bacon", action: :bacon, via: :get + # end + def controller: [T] (path controller) { () -> T } -> T + + # Scopes routes to a specific namespace. For example: + # + # namespace :admin do + # resources :posts + # end + # + # This generates the following routes: + # + # admin_posts GET /admin/posts(.:format) admin/posts#index + # admin_posts POST /admin/posts(.:format) admin/posts#create + # new_admin_post GET /admin/posts/new(.:format) admin/posts#new + # edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit + # admin_post GET /admin/posts/:id(.:format) admin/posts#show + # admin_post PATCH/PUT /admin/posts/:id(.:format) admin/posts#update + # admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy + # + # === Options + # + # The +:path+, +:as+, +:module+, +:shallow_path+ and +:shallow_prefix+ + # options all default to the name of the namespace. + # + # For options, see Base#match. For +:shallow_path+ option, see + # Resources#resources. + # + # # accessible through /sekret/posts rather than /admin/posts + # namespace :admin, path: "sekret" do + # resources :posts + # end + # + # # maps to Sekret::PostsController rather than Admin::PostsController + # namespace :admin, module: "sekret" do + # resources :posts + # end + # + # # generates +sekret_posts_path+ rather than +admin_posts_path+ + # namespace :admin, as: "sekret" do + # resources :posts + # end + def namespace: (path name, ?as: path, ?path: path, ?shallow_path: path, ?shallow_prefix: path, ?module: path, ?constraints: constraints, ?defaults: Hash[Symbol, untyped], **untyped) { () -> void } -> self + + # === Parameter Restriction + # Allows you to constrain the nested routes based on a set of rules. + # For instance, in order to change the routes to allow for a dot character in the +id+ parameter: + # + # constraints(id: /\d+\.\d+/) do + # resources :posts + # end + # + # Now routes such as +/posts/1+ will no longer be valid, but +/posts/1.1+ will be. + # The +id+ parameter must match the constraint passed in for this example. + # + # You may use this to also restrict other parameters: + # + # resources :posts do + # constraints(post_id: /\d+\.\d+/) do + # resources :comments + # end + # end + # + # === Restricting based on IP + # + # Routes can also be constrained to an IP or a certain range of IP addresses: + # + # constraints(ip: /192\.168\.\d+\.\d+/) do + # resources :posts + # end + # + # Any user connecting from the 192.168.* range will be able to see this resource, + # where as any user connecting outside of this range will be told there is no such route. + # + # === Dynamic request matching + # + # Requests to routes can be constrained based on specific criteria: + # + # constraints(-> (req) { req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do + # resources :iphones + # end + # + # You are able to move this logic out into a class if it is too complex for routes. + # This class must have a +matches?+ method defined on it which either returns +true+ + # if the user should be given access to that route, or +false+ if the user should not. + # + # class Iphone + # def self.matches?(request) + # request.env["HTTP_USER_AGENT"] =~ /iPhone/ + # end + # end + # + # An expected place for this code would be +lib/constraints+. + # + # This class is then used like this: + # + # constraints(Iphone) do + # resources :iphones + # end + def constraints: (?constraints constraints) { () -> void } -> self + + # Allows you to set default parameters for a route, such as this: + # defaults id: 'home' do + # match 'scoped_pages/(:id)', to: 'pages#show' + # end + # Using this, the +:id+ parameter here will default to 'home'. + def defaults: [T] (?Hash[Symbol, untyped] defaults) { () -> T } -> T + end + + module Concerns + # Define a routing concern using a name. + # + # Concerns may be defined inline, using a block, or handled by + # another object, by passing that object as the second parameter. + # + # The concern object, if supplied, should respond to call, + # which will receive two parameters: + # + # * The current mapper + # * A hash of options which the concern object may use + # + # Options may also be used by concerns defined in a block by accepting + # a block parameter. So, using a block, you might do something as + # simple as limit the actions available on certain resources, passing + # standard resource options through the concern: + # + # concern :commentable do |options| + # resources :comments, options + # end + # + # resources :posts, concerns: :commentable + # resources :archived_posts do + # # Don't allow comments on archived posts + # concerns :commentable, only: [:index, :show] + # end + # + # Or, using a callable object, you might implement something more + # specific to your application, which would be out of place in your + # routes file. + # + # # purchasable.rb + # class Purchasable + # def initialize(defaults = {}) + # @defaults = defaults + # end + # + # def call(mapper, options = {}) + # options = @defaults.merge(options) + # mapper.resources :purchases + # mapper.resources :receipts + # mapper.resources :returns if options[:returnable] + # end + # end + # + # # routes.rb + # concern :purchasable, Purchasable.new(returnable: true) + # + # resources :toys, concerns: :purchasable + # resources :electronics, concerns: :purchasable + # resources :pets do + # concerns :purchasable, returnable: false + # end + # + # Any routing helpers can be used inside a concern. If using a + # callable, they're accessible from the Mapper that's passed to + # call. + def concern: (Symbol name, (^(ActionDispatch::Routing::Mapper mapper, Hash[Symbol, untyped] options) -> void) callable) -> ^(ActionDispatch::Routing::Mapper mapper, Hash[Symbol, untyped] options) -> void + | (Symbol name, _Concern callable) -> _Concern + | (Symbol name) { (Hash[Symbol, untyped] options) [self: ActionDispatch::Routing::Mapper] -> void } -> Proc + + # Use the named concerns + # + # resources :posts do + # concerns :commentable + # end + # + # Concerns also work in any routes helper that you want to use: + # + # namespace :posts do + # concerns :commentable + # end + def concerns: (*(Symbol | Array[Symbol]) names, **untyped options) -> Array[Symbol] + end + module Resources # Sometimes, you have a resource that clients always look up without # referencing an ID. A common example, /profile always shows the @@ -141,7 +429,7 @@ module ActionDispatch # # === Options # Takes same options as resources[rdoc-ref:#resources] - def resource: (*untyped resources) ?{ () -> untyped } -> self + def resource: (*path resources, ?as: path, ?controller: path, ?path: path, ?only: actions, ?except: actions, ?param: path, ?concerns: Symbol | Array[Symbol], ?shallow: bool, ?module: path, ?constraints: constraints, ?defaults: Hash[Symbol, untyped], ?path_names: Hash[Symbol, String], ?shallow_path: path, ?shallow_prefix: path, ?format: format, **untyped) ?{ () -> void } -> self # In Rails, a resourceful route provides a mapping between HTTP verbs # and URLs and controller actions. By convention, each action also maps @@ -280,49 +568,67 @@ module ActionDispatch # # # resource actions are at /admin/posts. # resources :posts, path: "admin/posts" - def resources: (*untyped resources) ?{ () -> untyped } -> self - - def new: [T] () { () -> T } -> T + def resources: (*path resources, ?as: path, ?controller: path, ?path: path, ?only: actions, ?except: actions, ?param: path, ?concerns: Symbol | Array[Symbol], ?shallow: bool, ?module: path, ?constraints: constraints, ?defaults: Hash[Symbol, untyped], ?path_names: Hash[Symbol, String], ?shallow_path: path, ?shallow_prefix: path, ?format: format, **untyped) ?{ () -> void } -> self + # To add a route to the collection: + # + # resources :photos do + # collection do + # get 'search' + # end + # end + # + # This will enable Rails to recognize paths such as /photos/search + # with GET, and route to the search action of +PhotosController+. It will also + # create the search_photos_url and search_photos_path + # route helpers. def collection: [T] () { () -> T } -> T + # To add a member route, add a member block into the resource block: + # + # resources :photos do + # member do + # get 'preview' + # end + # end + # + # This will recognize /photos/1/preview with GET, and route to the + # preview action of +PhotosController+. It will also create the + # preview_photo_url and preview_photo_path helpers. def member: [T] () { () -> T } -> T + def new: [T] () { () -> T } -> T + def nested: () { () -> void } -> self - def namespace: (untyped path, ?::Hash[untyped, untyped] options) { () -> void } -> self + def namespace: (path name, ?as: path, ?path: path, ?shallow_path: path, ?shallow_prefix: path, ?module: path, ?constraints: constraints, ?defaults: Hash[Symbol, untyped], **untyped) { () -> void } -> self def shallow: [T] () { () -> T } -> T - def match: (untyped path, *untyped rest) ?{ () -> untyped } -> self - - def root: (untyped path, ?::Hash[untyped, untyped] options) -> self - end - - module Base - def mount: (untyped app, ?untyped? options) -> self - - def with_default_scope: (untyped scope) { () [self: ActionDispatch::Routing::Mapper] -> void } -> self - - def has_named_route?: (untyped name) -> bool - end - - module Scoping - def scope: (*untyped args) { () -> void } -> self - - def controller: [T] (untyped controller) { () -> T } -> T - - def namespace: (untyped path, ?::Hash[untyped, untyped] options) { () -> void } -> self - - def constraints: (?untyped constraints) { () -> void } -> self - - def defaults: [T] (?::Hash[untyped, untyped] defaults) { () -> T } -> T - end - - module Concerns - def concern: (untyped name, ?untyped? callable) ?{ (::Hash[untyped, untyped] options) [self: ActionDispatch::Routing::Mapper] -> void } -> untyped + # Matches a URL pattern to one or more routes. + # For more information, see match[rdoc-ref:Base#match]. + # + # match 'path' => 'controller#action', via: :patch + # match 'path', to: 'controller#action', via: :post + # match 'path', 'otherpath', on: :member, via: :get + def match: (path path, *path paths, ?as: path?, ?via: via, ?to: endpoint, ?controller: path, ?action: path, ?on: on, ?defaults: Hash[Symbol, untyped], ?constraints: constraints, ?anchor: bool, ?format: format, ?path: path, ?internal: bool, **untyped) -> self + | (Hash[path, untyped] path_to_endpoint_and_options) -> self - def concerns: (*untyped args) -> ::Array[untyped] + # You can specify what Rails should route "/" to with the root method: + # + # root to: 'pages#main' + # + # For options, see +match+, as +root+ uses it internally. + # + # You can also pass a string which will expand + # + # root 'pages#main' + # + # You should put the root route at the top of config/routes.rb, + # because this means it will be matched first. As this is the most popular route + # of most Rails applications, this is beneficial. + def root: (String to, ?as: path?, ?via: via, ?constraints: constraints, ?defaults: Hash[Symbol, untyped], ?format: format, **untyped) -> self + | (to: endpoint, ?as: path?, ?via: via, ?constraints: constraints, ?defaults: Hash[Symbol, untyped], ?format: format, **untyped) -> self end end end diff --git a/gems/actionpack/8.1/_test/test.rb b/gems/actionpack/8.1/_test/test.rb index 5794a7a5c..eb1b683c7 100644 --- a/gems/actionpack/8.1/_test/test.rb +++ b/gems/actionpack/8.1/_test/test.rb @@ -118,28 +118,53 @@ def show end end -# Routing DSL methods return the mapper or their block value +# Routing DSL forms from the Rails routing guide +class AdminConstraint + def matches?(request) + request.path.start_with?("/admin") + end +end + ActionDispatch::Routing::RouteSet.new.draw do - resources(:photos) do - collection { get :search }.has_named_route?(:search_photos) - member { "preview" }.upcase - new { 1 }.succ - shallow { [1] }.first - nested { resources :comments }.has_named_route?(:photo_comments) - namespace(:admin) { resource :stats }.has_named_route?(:admin_stats) - end.has_named_route?(:photos) - - scope(module: "admin") { get "dashboard", to: "dashboard#show" }.has_named_route?(:dashboard) - namespace(:api) { resources :posts }.has_named_route?(:api_posts) - constraints(subdomain: "api") { put "sync", to: "sync#update" }.has_named_route?(:sync) - controller(:pages) { 1 }.succ - defaults(format: :json) { "json" }.upcase + root "pages#home" + root(to: "pages#home", as: :home).first&.length + + get "profile", to: "users#show", as: :profile + get "photos/search" => "photos#search" + get "health", to: ->(_env) { [200, { "content-type" => "text/plain" }, ["OK"]] } + get "old", to: redirect("/new") + post "uploads", controller: :uploads, action: :create, defaults: { format: "json" }, format: false + match("photos", to: "photos#show", via: [:get, :post]).size + match "exports/:id", to: "exports#show", via: :all, constraints: { id: /\d+/ }, anchor: true + + resources :photos, only: [:index, :show], param: :slug, path: "pictures", as: :images do + member { get :preview }.has_named_route?(:preview_image) + collection { get :search }.has_named_route?(:search_images) + new { get :draft }.has_named_route?(:draft_new_image) + get :recent, on: :collection + nested { resources :comments, except: :destroy }.has_named_route?(:image_comments) + end.has_named_route?(:images) + resource :geocoder, controller: "geocoders", module: "maps" + resources :articles, concerns: [:commentable], path_names: { new: "make" }, shallow: true + shallow { resources(:posts) { resources :drafts } }.has_named_route?(:posts) + + namespace(:admin, path: "control", as: :ctl) { resources :articles }.has_named_route?(:ctl_articles) + scope(module: "admin", path: "manage", as: :manage) { resources :users }.has_named_route?(:manage_users) + scope("(:locale)", locale: /en|nl/) { resources :books } + scope(only: :index) { resources :tags } + + controller(:pages) { get "about", action: :about }.has_named_route?(:about) + defaults(format: :json) { get "status", to: "status#show" }.has_named_route?(:status) + + constraints(subdomain: "api") { resources :keys } + constraints(id: /\d+/) { get "items/:id", to: "items#show" } + constraints(->(request) { request.xhr? }) { get "async", to: "async#show" } + constraints(AdminConstraint.new) { get "admin", to: "admin#index" } + get "secure", to: "secure#show", constraints: ->(request) { request.ssl? } + + concern(:commentable) { |options| resources :comments, **options } + concerns(:commentable, only: :index).first&.to_proc + + mount Rack::Head.new(->(_env) { [200, { "content-type" => "text/plain" }, ["OK"]] }), at: "/head", as: :head with_default_scope(module: "v1") { get "ping", to: "ping#show" }.has_named_route?(:ping) - - concern(:taggable) { |options| options.size } - concerns(:taggable).size - - get("status", to: "status#show").post("status", to: "status#create").has_named_route?(:status) - match("/hello", to: "hello#index", via: :get).size - root("home#index").size end diff --git a/gems/actionpack/8.1/_test/test.rbs b/gems/actionpack/8.1/_test/test.rbs index d568eb0ab..1b4162bb3 100644 --- a/gems/actionpack/8.1/_test/test.rbs +++ b/gems/actionpack/8.1/_test/test.rbs @@ -24,3 +24,7 @@ end class AuthController < ActionController::Base def show: () -> void end + +class AdminConstraint + def matches?: (ActionDispatch::Request request) -> bool +end diff --git a/gems/actionpack/8.1/actiondispatch.rbs b/gems/actionpack/8.1/actiondispatch.rbs index d130b672d..1e8f3b4b2 100644 --- a/gems/actionpack/8.1/actiondispatch.rbs +++ b/gems/actionpack/8.1/actiondispatch.rbs @@ -81,36 +81,338 @@ module ActionDispatch end class Mapper + # A route path, resource name, controller, or action. + type path = String | Symbol + + # HTTP verbs the route recognizes, such as `:get`, `[:get, :post]`, or `:all`. + type via = Symbol | Array[Symbol] + + # `true` requires a format segment, `false` drops it, and a String or Regexp constrains it. + type format = bool | String | Regexp | nil + + # Values accepted by the `:only` and `:except` resource options. + type actions = path | Array[path] + + # Values accepted by the `:to` option: `"controller#action"`, an action name, a Rack application, or a controller class. + type endpoint = String | ^(Rack::env env) -> Rack::response | Rack::_Application | singleton(ActionController::Metal) + + # Request constraints are objects responding to `matches?`, or callables taking the request. + interface _RequestMatcher + def matches?: (ActionDispatch::Request request) -> boolish + end + + type request_constraint = ^(ActionDispatch::Request request) -> boolish | _RequestMatcher + + # Segment constraints (`{ id: /\d+/ }`) and request conditions (`{ subdomain: "api" }`), or a request constraint. + type constraints = Hash[Symbol, untyped] | request_constraint + + # The scope a member, collection, or new route is added to. + type on = :member | :collection | :new + + # A concern defined with a callable receives the mapper and the options given to `concerns`. + interface _Concern + def call: (ActionDispatch::Routing::Mapper mapper, Hash[Symbol, untyped] options) -> void + end + + module Base + # Mount a Rack-based application to be used within the application. + # + # mount SomeRackApp, at: "some_route" + # + # For options, see `match`, as `mount` uses it internally. + # + # All mounted applications come with routing helpers to access them. These are + # named after the class specified, so for the above example the helper is either + # `some_rack_app_path` or `some_rack_app_url`. To customize this helper's name, + # use the `:as` option: + # + # mount(SomeRackApp, at: "some_route", as: "exciting") + # + # This will generate the `exciting_path` and `exciting_url` helpers which can be + # used to navigate to this mounted app. + def mount: (untyped app, at: String, ?as: path?, ?via: via, ?defaults: Hash[Symbol, untyped], ?constraints: constraints, ?anchor: bool, ?format: format, ?path: path, ?internal: bool, **untyped) -> self + | (Hash[untyped, untyped] app_to_path_and_options) -> self + + def with_default_scope: (Hash[Symbol, untyped] scope) { () [self: ActionDispatch::Routing::Mapper] -> void } -> self + + # Query if the following named route was already defined. + def has_named_route?: (path name) -> bool + end + module HttpHelpers # Define a route that only recognizes HTTP GET. # For supported arguments, see match[rdoc-ref:Base#match] # # get 'bacon', to: 'food#bacon' - def get: (*untyped args) ?{ () -> untyped } -> self + def get: (path path, ?as: path?, ?to: endpoint, ?controller: path, ?action: path, ?on: on, ?defaults: Hash[Symbol, untyped], ?constraints: constraints, ?anchor: bool, ?format: format, ?path: path, ?internal: bool, **untyped) -> self + | (Hash[path, untyped] path_to_endpoint_and_options) -> self # Define a route that only recognizes HTTP POST. # For supported arguments, see match[rdoc-ref:Base#match] # # post 'bacon', to: 'food#bacon' - def post: (*untyped args) ?{ () -> untyped } -> self + def post: (path path, ?as: path?, ?to: endpoint, ?controller: path, ?action: path, ?on: on, ?defaults: Hash[Symbol, untyped], ?constraints: constraints, ?anchor: bool, ?format: format, ?path: path, ?internal: bool, **untyped) -> self + | (Hash[path, untyped] path_to_endpoint_and_options) -> self # Define a route that only recognizes HTTP PATCH. # For supported arguments, see match[rdoc-ref:Base#match] # # patch 'bacon', to: 'food#bacon' - def patch: (*untyped args) ?{ () -> untyped } -> self + def patch: (path path, ?as: path?, ?to: endpoint, ?controller: path, ?action: path, ?on: on, ?defaults: Hash[Symbol, untyped], ?constraints: constraints, ?anchor: bool, ?format: format, ?path: path, ?internal: bool, **untyped) -> self + | (Hash[path, untyped] path_to_endpoint_and_options) -> self # Define a route that only recognizes HTTP PUT. # For supported arguments, see match[rdoc-ref:Base#match] # # put 'bacon', to: 'food#bacon' - def put: (*untyped args) ?{ () -> untyped } -> self + def put: (path path, ?as: path?, ?to: endpoint, ?controller: path, ?action: path, ?on: on, ?defaults: Hash[Symbol, untyped], ?constraints: constraints, ?anchor: bool, ?format: format, ?path: path, ?internal: bool, **untyped) -> self + | (Hash[path, untyped] path_to_endpoint_and_options) -> self # Define a route that only recognizes HTTP DELETE. # For supported arguments, see match[rdoc-ref:Base#match] # # delete 'broccoli', to: 'food#broccoli' - def delete: (*untyped args) ?{ () -> untyped } -> self + def delete: (path path, ?as: path?, ?to: endpoint, ?controller: path, ?action: path, ?on: on, ?defaults: Hash[Symbol, untyped], ?constraints: constraints, ?anchor: bool, ?format: format, ?path: path, ?internal: bool, **untyped) -> self + | (Hash[path, untyped] path_to_endpoint_and_options) -> self + + # Define a route that only recognizes HTTP OPTIONS. For supported arguments, see + # [match](rdoc-ref:Base#match) + # + # options 'carrots', to: 'food#carrots' + def options: (path path, ?as: path?, ?to: endpoint, ?controller: path, ?action: path, ?on: on, ?defaults: Hash[Symbol, untyped], ?constraints: constraints, ?anchor: bool, ?format: format, ?path: path, ?internal: bool, **untyped) -> self + | (Hash[path, untyped] path_to_endpoint_and_options) -> self + + # Define a route that recognizes HTTP CONNECT (and GET) requests. More + # specifically this recognizes HTTP/1 protocol upgrade requests and HTTP/2 + # CONNECT requests with the protocol pseudo header. For supported arguments, + # see [match](rdoc-ref:Base#match) + # + # connect 'live', to: 'live#index' + def connect: (path path, ?as: path?, ?to: endpoint, ?controller: path, ?action: path, ?on: on, ?defaults: Hash[Symbol, untyped], ?constraints: constraints, ?anchor: bool, ?format: format, ?path: path, ?internal: bool, **untyped) -> self + | (Hash[path, untyped] path_to_endpoint_and_options) -> self + end + + module Scoping + # Scopes a set of routes to the given default options. + # + # Take the following route definition as an example: + # + # scope path: ":account_id", as: "account" do + # resources :projects + # end + # + # This generates helpers such as `account_projects_path`, just like `resources` + # does. The difference here being that the routes generated are like + # /:account_id/projects, rather than /accounts/:account_id/projects. + # + # ### Options + # + # Takes same options as `Base#match` and `Resources#resources`. + # + # # route /posts (without the prefix /admin) to Admin::PostsController + # scope module: "admin" do + # resources :posts + # end + # + # # prefix the posts resource's requests with '/admin' + # scope path: "/admin" do + # resources :posts + # end + # + # # prefix the routing helper name: sekret_posts_path instead of posts_path + # scope as: "sekret" do + # resources :posts + # end + def scope: (*(path | Array[path]) paths, ?only: actions, ?except: actions, ?module: path, ?path: path, ?as: path, ?controller: path, ?action: path, ?constraints: constraints, ?defaults: Hash[Symbol, untyped], ?shallow: bool, ?shallow_path: path, ?shallow_prefix: path, ?path_names: Hash[Symbol, String], ?format: format, ?via: via, ?to: endpoint, **untyped) { () -> void } -> self + + # Scopes routes to a specific controller + # + # controller "food" do + # match "bacon", action: :bacon, via: :get + # end + def controller: [T] (path controller) { () -> T } -> T + + # Scopes routes to a specific namespace. For example: + # + # namespace :admin do + # resources :posts + # end + # + # This generates the following routes: + # + # admin_posts GET /admin/posts(.:format) admin/posts#index + # admin_posts POST /admin/posts(.:format) admin/posts#create + # new_admin_post GET /admin/posts/new(.:format) admin/posts#new + # edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit + # admin_post GET /admin/posts/:id(.:format) admin/posts#show + # admin_post PATCH/PUT /admin/posts/:id(.:format) admin/posts#update + # admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy + # + # ### Options + # + # The `:path`, `:as`, `:module`, `:shallow_path`, and `:shallow_prefix` options + # all default to the name of the namespace. + # + # For options, see `Base#match`. For `:shallow_path` option, see + # `Resources#resources`. + # + # # accessible through /sekret/posts rather than /admin/posts + # namespace :admin, path: "sekret" do + # resources :posts + # end + # + # # maps to Sekret::PostsController rather than Admin::PostsController + # namespace :admin, module: "sekret" do + # resources :posts + # end + # + # # generates sekret_posts_path rather than admin_posts_path + # namespace :admin, as: "sekret" do + # resources :posts + # end + def namespace: (path name, ?as: path, ?path: path, ?shallow_path: path, ?shallow_prefix: path, ?module: path, ?constraints: constraints, ?defaults: Hash[Symbol, untyped], **untyped) { () -> void } -> self + + # ### Parameter Restriction + # Allows you to constrain the nested routes based on a set of rules. For + # instance, in order to change the routes to allow for a dot character in the + # `id` parameter: + # + # constraints(id: /\d+\.\d+/) do + # resources :posts + # end + # + # Now routes such as `/posts/1` will no longer be valid, but `/posts/1.1` will + # be. The `id` parameter must match the constraint passed in for this example. + # + # You may use this to also restrict other parameters: + # + # resources :posts do + # constraints(post_id: /\d+\.\d+/) do + # resources :comments + # end + # end + # + # ### Restricting based on IP + # + # Routes can also be constrained to an IP or a certain range of IP addresses: + # + # constraints(ip: /192\.168\.\d+\.\d+/) do + # resources :posts + # end + # + # Any user connecting from the 192.168.* range will be able to see this + # resource, where as any user connecting outside of this range will be told + # there is no such route. + # + # ### Dynamic request matching + # + # Requests to routes can be constrained based on specific criteria: + # + # constraints(-> (req) { /iPhone/.match?(req.env["HTTP_USER_AGENT"]) }) do + # resources :iphones + # end + # + # You are able to move this logic out into a class if it is too complex for + # routes. This class must have a `matches?` method defined on it which either + # returns `true` if the user should be given access to that route, or `false` if + # the user should not. + # + # class Iphone + # def self.matches?(request) + # /iPhone/.match?(request.env["HTTP_USER_AGENT"]) + # end + # end + # + # An expected place for this code would be `lib/constraints`. + # + # This class is then used like this: + # + # constraints(Iphone) do + # resources :iphones + # end + def constraints: (?constraints constraints) { () -> void } -> self + + # Allows you to set default parameters for a route, such as this: + # + # defaults id: 'home' do + # match 'scoped_pages/(:id)', to: 'pages#show' + # end + # + # Using this, the `:id` parameter here will default to 'home'. + def defaults: [T] (?Hash[Symbol, untyped] defaults) { () -> T } -> T + end + + module Concerns + # Define a routing concern using a name. + # + # Concerns may be defined inline, using a block, or handled by another object, + # by passing that object as the second parameter. + # + # The concern object, if supplied, should respond to `call`, which will receive + # two parameters: + # + # * The current mapper + # * A hash of options which the concern object may use + # + # Options may also be used by concerns defined in a block by accepting a block + # parameter. So, using a block, you might do something as simple as limit the + # actions available on certain resources, passing standard resource options + # through the concern: + # + # concern :commentable do |options| + # resources :comments, options + # end + # + # resources :posts, concerns: :commentable + # resources :archived_posts do + # # Don't allow comments on archived posts + # concerns :commentable, only: [:index, :show] + # end + # + # Or, using a callable object, you might implement something more specific to + # your application, which would be out of place in your routes file. + # + # # purchasable.rb + # class Purchasable + # def initialize(defaults = {}) + # @defaults = defaults + # end + # + # def call(mapper, options = {}) + # options = @defaults.merge(options) + # mapper.resources :purchases + # mapper.resources :receipts + # mapper.resources :returns if options[:returnable] + # end + # end + # + # # routes.rb + # concern :purchasable, Purchasable.new(returnable: true) + # + # resources :toys, concerns: :purchasable + # resources :electronics, concerns: :purchasable + # resources :pets do + # concerns :purchasable, returnable: false + # end + # + # Any routing helpers can be used inside a concern. If using a callable, they're + # accessible from the Mapper that's passed to `call`. + def concern: (Symbol name, (^(ActionDispatch::Routing::Mapper mapper, Hash[Symbol, untyped] options) -> void) callable) -> ^(ActionDispatch::Routing::Mapper mapper, Hash[Symbol, untyped] options) -> void + | (Symbol name, _Concern callable) -> _Concern + | (Symbol name) { (Hash[Symbol, untyped] options) [self: ActionDispatch::Routing::Mapper] -> void } -> Proc + + # Use the named concerns + # + # resources :posts do + # concerns :commentable + # end + # + # Concerns also work in any routes helper that you want to use: + # + # namespace :posts do + # concerns :commentable + # end + def concerns: (*(Symbol | Array[Symbol]) names, **untyped options) -> Array[Symbol] end module Resources @@ -135,7 +437,7 @@ module ActionDispatch # # === Options # Takes same options as resources[rdoc-ref:#resources] - def resource: (*untyped resources) ?{ () -> untyped } -> self + def resource: (*path resources, ?as: path, ?controller: path, ?path: path, ?only: actions, ?except: actions, ?param: path, ?concerns: Symbol | Array[Symbol], ?shallow: bool, ?module: path, ?constraints: constraints, ?defaults: Hash[Symbol, untyped], ?path_names: Hash[Symbol, String], ?shallow_path: path, ?shallow_prefix: path, ?format: format, **untyped) ?{ () -> void } -> self # In Rails, a resourceful route provides a mapping between HTTP verbs # and URLs and controller actions. By convention, each action also maps @@ -274,47 +576,66 @@ module ActionDispatch # # # resource actions are at /admin/posts. # resources :posts, path: "admin/posts" - def resources: (*untyped resources) ?{ () -> untyped } -> self - - def new: [T] () { () -> T } -> T + def resources: (*path resources, ?as: path, ?controller: path, ?path: path, ?only: actions, ?except: actions, ?param: path, ?concerns: Symbol | Array[Symbol], ?shallow: bool, ?module: path, ?constraints: constraints, ?defaults: Hash[Symbol, untyped], ?path_names: Hash[Symbol, String], ?shallow_path: path, ?shallow_prefix: path, ?format: format, **untyped) ?{ () -> void } -> self + # To add a route to the collection: + # + # resources :photos do + # collection do + # get 'search' + # end + # end + # + # This will enable Rails to recognize paths such as `/photos/search` with GET, + # and route to the search action of `PhotosController`. It will also create the + # `search_photos_url` and `search_photos_path` route helpers. def collection: [T] () { () -> T } -> T + # To add a member route, add a member block into the resource block: + # + # resources :photos do + # member do + # get 'preview' + # end + # end + # + # This will recognize `/photos/1/preview` with GET, and route to the preview + # action of `PhotosController`. It will also create the `preview_photo_url` and + # `preview_photo_path` helpers. def member: [T] () { () -> T } -> T + def new: [T] () { () -> T } -> T + def nested: () { () -> void } -> self - def namespace: (untyped name, ?untyped? deprecated_options, ?as: untyped, ?path: untyped, ?shallow_path: untyped, ?shallow_prefix: untyped, **untyped options) { () -> void } -> self + # See ActionDispatch::Routing::Mapper::Scoping#namespace. + def namespace: (path name, ?as: path, ?path: path, ?shallow_path: path, ?shallow_prefix: path, ?module: path, ?constraints: constraints, ?defaults: Hash[Symbol, untyped], **untyped) { () -> void } -> self def shallow: [T] () { () -> T } -> T - def match: (*untyped path_or_actions, ?as: untyped, ?via: untyped?, ?to: untyped?, ?controller: untyped?, ?action: untyped?, ?on: untyped?, ?defaults: untyped?, ?constraints: untyped?, ?anchor: untyped?, ?format: untyped?, ?path: untyped?, ?internal: untyped?, **untyped mapping) ?{ (?) -> untyped } -> ::Array[untyped] - - def root: (untyped path, ?::Hash[untyped, untyped] options) -> ::Array[untyped] - end - - module Base - def with_default_scope: (untyped scope) { () [self: ActionDispatch::Routing::Mapper] -> void } -> self - - def has_named_route?: (untyped name) -> bool - end - - module Scoping - def scope: (*untyped args, ?only: untyped?, ?except: untyped?, **untyped options) { () -> void } -> self - - def controller: [T] (untyped controller) { () -> T } -> T - - def namespace: (untyped name, ?untyped? deprecated_options, ?as: untyped, ?path: untyped, ?shallow_path: untyped, ?shallow_prefix: untyped, **untyped options) { () -> void } -> self - - def constraints: (?untyped constraints) { () -> void } -> self - - def defaults: [T] (?::Hash[untyped, untyped] defaults) { () -> T } -> T - end - - module Concerns - def concern: (untyped name, ?untyped? callable) ?{ (::Hash[Symbol, untyped] options) [self: ActionDispatch::Routing::Mapper] -> void } -> untyped + # Matches a URL pattern to one or more routes. For more information, see + # [match](rdoc-ref:Base#match). + # + # match 'path', to: 'controller#action', via: :post + # match 'otherpath', on: :member, via: :get + def match: (path path, ?as: path?, ?via: via, ?to: endpoint, ?controller: path, ?action: path, ?on: on, ?defaults: Hash[Symbol, untyped], ?constraints: constraints, ?anchor: bool, ?format: format, ?path: path, ?internal: bool, **untyped) -> Array[path] + | (Hash[path, untyped] path_to_endpoint_and_options) -> Array[path] - def concerns: (*untyped args, **untyped options) -> ::Array[untyped] + # You can specify what Rails should route "/" to with the root method: + # + # root to: 'pages#main' + # + # For options, see `match`, as `root` uses it internally. + # + # You can also pass a string which will expand + # + # root 'pages#main' + # + # You should put the root route at the top of `config/routes.rb`, because this + # means it will be matched first. As this is the most popular route of most + # Rails applications, this is beneficial. + def root: (String to, ?as: path?, ?via: via, ?constraints: constraints, ?defaults: Hash[Symbol, untyped], ?format: format, **untyped) -> Array[String] + | (to: endpoint, ?as: path?, ?via: via, ?constraints: constraints, ?defaults: Hash[Symbol, untyped], ?format: format, **untyped) -> Array[String] end end end diff --git a/gems/actionpack/8.1/actionpack-generated.rbs b/gems/actionpack/8.1/actionpack-generated.rbs index f798926f8..b4434d68a 100644 --- a/gems/actionpack/8.1/actionpack-generated.rbs +++ b/gems/actionpack/8.1/actionpack-generated.rbs @@ -10663,23 +10663,6 @@ module ActionDispatch # def match: (untyped path, ?untyped? options) -> nil - # Mount a Rack-based application to be used within the application. - # - # mount SomeRackApp, at: "some_route" - # - # For options, see `match`, as `mount` uses it internally. - # - # All mounted applications come with routing helpers to access them. These are - # named after the class specified, so for the above example the helper is either - # `some_rack_app_path` or `some_rack_app_url`. To customize this helper's name, - # use the `:as` option: - # - # mount(SomeRackApp, at: "some_route", as: "exciting") - # - # This will generate the `exciting_path` and `exciting_url` helpers which can be - # used to navigate to this mounted app. - def mount: (?untyped? app, ?untyped? deprecated_options, ?as: untyped, ?via: untyped?, ?at: untyped?, ?defaults: untyped?, ?constraints: untyped?, ?anchor: bool, ?format: bool, ?path: untyped?, ?internal: untyped?, **untyped mapping) ?{ (?) -> untyped } -> self - def default_url_options=: (untyped options) -> untyped alias default_url_options default_url_options= @@ -10698,19 +10681,6 @@ module ActionDispatch end module HttpHelpers - # Define a route that only recognizes HTTP OPTIONS. For supported arguments, see - # [match](rdoc-ref:Base#match) - # - # options 'carrots', to: 'food#carrots' - def options: (*untyped path_or_actions, ?as: untyped, ?to: untyped?, ?controller: untyped?, ?action: untyped?, ?on: untyped?, ?defaults: untyped?, ?constraints: untyped?, ?anchor: bool, ?format: bool, ?path: untyped?, ?internal: untyped?, **untyped mapping) ?{ (?) -> untyped } -> self - - # Define a route that recognizes HTTP CONNECT (and GET) requests. More - # specifically this recognizes HTTP/1 protocol upgrade requests and HTTP/2 - # CONNECT requests with the protocol pseudo header. For supported arguments, - # see [match](rdoc-ref:Base#match) - # - # connect 'live', to: 'live#index' - def connect: (*untyped path_or_actions, ?as: untyped, ?to: untyped?, ?controller: untyped?, ?action: untyped?, ?on: untyped?, ?defaults: untyped?, ?constraints: untyped?, ?anchor: bool, ?format: bool, ?path: untyped?, ?internal: untyped?, **untyped mapping) ?{ (?) -> untyped } -> self end # You may wish to organize groups of controllers under a namespace. Most From b2fa2c92c5a48a447af7ceb2b5d03a16647c87ca Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 19 Oct 2026 14:11:28 -0400 Subject: [PATCH 5/5] actionpack: return the lambda from block concern concern with a block stores and returns the same mapper lambda shape as concern with a lambda, so type both overloads alike. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01SEntZKsZwQHhzqcwG7MTqn --- gems/actionpack/7.2/actiondispatch.rbs | 2 +- gems/actionpack/8.1/actiondispatch.rbs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gems/actionpack/7.2/actiondispatch.rbs b/gems/actionpack/7.2/actiondispatch.rbs index 8c0d69547..1786cb68e 100644 --- a/gems/actionpack/7.2/actiondispatch.rbs +++ b/gems/actionpack/7.2/actiondispatch.rbs @@ -391,7 +391,7 @@ module ActionDispatch # call. def concern: (Symbol name, (^(ActionDispatch::Routing::Mapper mapper, Hash[Symbol, untyped] options) -> void) callable) -> ^(ActionDispatch::Routing::Mapper mapper, Hash[Symbol, untyped] options) -> void | (Symbol name, _Concern callable) -> _Concern - | (Symbol name) { (Hash[Symbol, untyped] options) [self: ActionDispatch::Routing::Mapper] -> void } -> Proc + | (Symbol name) { (Hash[Symbol, untyped] options) [self: ActionDispatch::Routing::Mapper] -> void } -> ^(ActionDispatch::Routing::Mapper mapper, Hash[Symbol, untyped] options) -> void # Use the named concerns # diff --git a/gems/actionpack/8.1/actiondispatch.rbs b/gems/actionpack/8.1/actiondispatch.rbs index 1e8f3b4b2..aa2beb84a 100644 --- a/gems/actionpack/8.1/actiondispatch.rbs +++ b/gems/actionpack/8.1/actiondispatch.rbs @@ -399,7 +399,7 @@ module ActionDispatch # accessible from the Mapper that's passed to `call`. def concern: (Symbol name, (^(ActionDispatch::Routing::Mapper mapper, Hash[Symbol, untyped] options) -> void) callable) -> ^(ActionDispatch::Routing::Mapper mapper, Hash[Symbol, untyped] options) -> void | (Symbol name, _Concern callable) -> _Concern - | (Symbol name) { (Hash[Symbol, untyped] options) [self: ActionDispatch::Routing::Mapper] -> void } -> Proc + | (Symbol name) { (Hash[Symbol, untyped] options) [self: ActionDispatch::Routing::Mapper] -> void } -> ^(ActionDispatch::Routing::Mapper mapper, Hash[Symbol, untyped] options) -> void # Use the named concerns #