omg-actionpack 8.0.0.alpha1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +129 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +57 -0
- data/lib/abstract_controller/asset_paths.rb +14 -0
- data/lib/abstract_controller/base.rb +299 -0
- data/lib/abstract_controller/caching/fragments.rb +149 -0
- data/lib/abstract_controller/caching.rb +68 -0
- data/lib/abstract_controller/callbacks.rb +265 -0
- data/lib/abstract_controller/collector.rb +44 -0
- data/lib/abstract_controller/deprecator.rb +9 -0
- data/lib/abstract_controller/error.rb +8 -0
- data/lib/abstract_controller/helpers.rb +243 -0
- data/lib/abstract_controller/logger.rb +16 -0
- data/lib/abstract_controller/railties/routes_helpers.rb +25 -0
- data/lib/abstract_controller/rendering.rb +126 -0
- data/lib/abstract_controller/translation.rb +42 -0
- data/lib/abstract_controller/url_for.rb +37 -0
- data/lib/abstract_controller.rb +36 -0
- data/lib/action_controller/api/api_rendering.rb +18 -0
- data/lib/action_controller/api.rb +155 -0
- data/lib/action_controller/base.rb +332 -0
- data/lib/action_controller/caching.rb +49 -0
- data/lib/action_controller/deprecator.rb +9 -0
- data/lib/action_controller/form_builder.rb +55 -0
- data/lib/action_controller/log_subscriber.rb +96 -0
- data/lib/action_controller/metal/allow_browser.rb +123 -0
- data/lib/action_controller/metal/basic_implicit_render.rb +17 -0
- data/lib/action_controller/metal/conditional_get.rb +341 -0
- data/lib/action_controller/metal/content_security_policy.rb +86 -0
- data/lib/action_controller/metal/cookies.rb +20 -0
- data/lib/action_controller/metal/data_streaming.rb +154 -0
- data/lib/action_controller/metal/default_headers.rb +21 -0
- data/lib/action_controller/metal/etag_with_flash.rb +22 -0
- data/lib/action_controller/metal/etag_with_template_digest.rb +59 -0
- data/lib/action_controller/metal/exceptions.rb +106 -0
- data/lib/action_controller/metal/flash.rb +67 -0
- data/lib/action_controller/metal/head.rb +67 -0
- data/lib/action_controller/metal/helpers.rb +129 -0
- data/lib/action_controller/metal/http_authentication.rb +565 -0
- data/lib/action_controller/metal/implicit_render.rb +67 -0
- data/lib/action_controller/metal/instrumentation.rb +120 -0
- data/lib/action_controller/metal/live.rb +398 -0
- data/lib/action_controller/metal/logging.rb +22 -0
- data/lib/action_controller/metal/mime_responds.rb +337 -0
- data/lib/action_controller/metal/parameter_encoding.rb +84 -0
- data/lib/action_controller/metal/params_wrapper.rb +312 -0
- data/lib/action_controller/metal/permissions_policy.rb +38 -0
- data/lib/action_controller/metal/rate_limiting.rb +62 -0
- data/lib/action_controller/metal/redirecting.rb +251 -0
- data/lib/action_controller/metal/renderers.rb +181 -0
- data/lib/action_controller/metal/rendering.rb +260 -0
- data/lib/action_controller/metal/request_forgery_protection.rb +667 -0
- data/lib/action_controller/metal/rescue.rb +33 -0
- data/lib/action_controller/metal/streaming.rb +183 -0
- data/lib/action_controller/metal/strong_parameters.rb +1546 -0
- data/lib/action_controller/metal/testing.rb +25 -0
- data/lib/action_controller/metal/url_for.rb +65 -0
- data/lib/action_controller/metal.rb +339 -0
- data/lib/action_controller/railtie.rb +149 -0
- data/lib/action_controller/railties/helpers.rb +26 -0
- data/lib/action_controller/renderer.rb +161 -0
- data/lib/action_controller/template_assertions.rb +13 -0
- data/lib/action_controller/test_case.rb +691 -0
- data/lib/action_controller.rb +80 -0
- data/lib/action_dispatch/constants.rb +34 -0
- data/lib/action_dispatch/deprecator.rb +9 -0
- data/lib/action_dispatch/http/cache.rb +249 -0
- data/lib/action_dispatch/http/content_disposition.rb +47 -0
- data/lib/action_dispatch/http/content_security_policy.rb +365 -0
- data/lib/action_dispatch/http/filter_parameters.rb +80 -0
- data/lib/action_dispatch/http/filter_redirect.rb +50 -0
- data/lib/action_dispatch/http/headers.rb +134 -0
- data/lib/action_dispatch/http/mime_negotiation.rb +187 -0
- data/lib/action_dispatch/http/mime_type.rb +389 -0
- data/lib/action_dispatch/http/mime_types.rb +54 -0
- data/lib/action_dispatch/http/parameters.rb +119 -0
- data/lib/action_dispatch/http/permissions_policy.rb +189 -0
- data/lib/action_dispatch/http/rack_cache.rb +67 -0
- data/lib/action_dispatch/http/request.rb +498 -0
- data/lib/action_dispatch/http/response.rb +556 -0
- data/lib/action_dispatch/http/upload.rb +107 -0
- data/lib/action_dispatch/http/url.rb +344 -0
- data/lib/action_dispatch/journey/formatter.rb +226 -0
- data/lib/action_dispatch/journey/gtg/builder.rb +149 -0
- data/lib/action_dispatch/journey/gtg/simulator.rb +50 -0
- data/lib/action_dispatch/journey/gtg/transition_table.rb +217 -0
- data/lib/action_dispatch/journey/nfa/dot.rb +27 -0
- data/lib/action_dispatch/journey/nodes/node.rb +208 -0
- data/lib/action_dispatch/journey/parser.rb +103 -0
- data/lib/action_dispatch/journey/path/pattern.rb +209 -0
- data/lib/action_dispatch/journey/route.rb +189 -0
- data/lib/action_dispatch/journey/router/utils.rb +105 -0
- data/lib/action_dispatch/journey/router.rb +151 -0
- data/lib/action_dispatch/journey/routes.rb +82 -0
- data/lib/action_dispatch/journey/scanner.rb +70 -0
- data/lib/action_dispatch/journey/visitors.rb +267 -0
- data/lib/action_dispatch/journey/visualizer/fsm.css +30 -0
- data/lib/action_dispatch/journey/visualizer/fsm.js +159 -0
- data/lib/action_dispatch/journey/visualizer/index.html.erb +52 -0
- data/lib/action_dispatch/journey.rb +7 -0
- data/lib/action_dispatch/log_subscriber.rb +25 -0
- data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
- data/lib/action_dispatch/middleware/assume_ssl.rb +27 -0
- data/lib/action_dispatch/middleware/callbacks.rb +38 -0
- data/lib/action_dispatch/middleware/cookies.rb +719 -0
- data/lib/action_dispatch/middleware/debug_exceptions.rb +206 -0
- data/lib/action_dispatch/middleware/debug_locks.rb +129 -0
- data/lib/action_dispatch/middleware/debug_view.rb +73 -0
- data/lib/action_dispatch/middleware/exception_wrapper.rb +350 -0
- data/lib/action_dispatch/middleware/executor.rb +32 -0
- data/lib/action_dispatch/middleware/flash.rb +318 -0
- data/lib/action_dispatch/middleware/host_authorization.rb +171 -0
- data/lib/action_dispatch/middleware/public_exceptions.rb +64 -0
- data/lib/action_dispatch/middleware/reloader.rb +16 -0
- data/lib/action_dispatch/middleware/remote_ip.rb +199 -0
- data/lib/action_dispatch/middleware/request_id.rb +50 -0
- data/lib/action_dispatch/middleware/server_timing.rb +78 -0
- data/lib/action_dispatch/middleware/session/abstract_store.rb +112 -0
- data/lib/action_dispatch/middleware/session/cache_store.rb +66 -0
- data/lib/action_dispatch/middleware/session/cookie_store.rb +129 -0
- data/lib/action_dispatch/middleware/session/mem_cache_store.rb +34 -0
- data/lib/action_dispatch/middleware/show_exceptions.rb +88 -0
- data/lib/action_dispatch/middleware/ssl.rb +180 -0
- data/lib/action_dispatch/middleware/stack.rb +194 -0
- data/lib/action_dispatch/middleware/static.rb +192 -0
- data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
- data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
- data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
- data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +17 -0
- data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +23 -0
- data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +36 -0
- data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
- data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +62 -0
- data/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +9 -0
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +12 -0
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +9 -0
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +35 -0
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +9 -0
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +16 -0
- data/lib/action_dispatch/middleware/templates/rescues/layout.erb +284 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +23 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +11 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb +3 -0
- data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +32 -0
- data/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb +11 -0
- data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +20 -0
- data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +7 -0
- data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +6 -0
- data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +3 -0
- data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +19 -0
- data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +232 -0
- data/lib/action_dispatch/railtie.rb +77 -0
- data/lib/action_dispatch/request/session.rb +283 -0
- data/lib/action_dispatch/request/utils.rb +109 -0
- data/lib/action_dispatch/routing/endpoint.rb +19 -0
- data/lib/action_dispatch/routing/inspector.rb +323 -0
- data/lib/action_dispatch/routing/mapper.rb +2372 -0
- data/lib/action_dispatch/routing/polymorphic_routes.rb +363 -0
- data/lib/action_dispatch/routing/redirection.rb +218 -0
- data/lib/action_dispatch/routing/route_set.rb +958 -0
- data/lib/action_dispatch/routing/routes_proxy.rb +66 -0
- data/lib/action_dispatch/routing/url_for.rb +244 -0
- data/lib/action_dispatch/routing.rb +262 -0
- data/lib/action_dispatch/system_test_case.rb +206 -0
- data/lib/action_dispatch/system_testing/browser.rb +75 -0
- data/lib/action_dispatch/system_testing/driver.rb +85 -0
- data/lib/action_dispatch/system_testing/server.rb +33 -0
- data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +164 -0
- data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +23 -0
- data/lib/action_dispatch/testing/assertion_response.rb +48 -0
- data/lib/action_dispatch/testing/assertions/response.rb +114 -0
- data/lib/action_dispatch/testing/assertions/routing.rb +343 -0
- data/lib/action_dispatch/testing/assertions.rb +25 -0
- data/lib/action_dispatch/testing/integration.rb +694 -0
- data/lib/action_dispatch/testing/request_encoder.rb +60 -0
- data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
- data/lib/action_dispatch/testing/test_process.rb +57 -0
- data/lib/action_dispatch/testing/test_request.rb +73 -0
- data/lib/action_dispatch/testing/test_response.rb +58 -0
- data/lib/action_dispatch.rb +147 -0
- data/lib/action_pack/gem_version.rb +19 -0
- data/lib/action_pack/version.rb +12 -0
- data/lib/action_pack.rb +27 -0
- metadata +375 -0
|
@@ -0,0 +1,2372 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# :markup: markdown
|
|
4
|
+
|
|
5
|
+
require "active_support/core_ext/hash/slice"
|
|
6
|
+
require "active_support/core_ext/enumerable"
|
|
7
|
+
require "active_support/core_ext/array/extract_options"
|
|
8
|
+
require "active_support/core_ext/regexp"
|
|
9
|
+
require "action_dispatch/routing/redirection"
|
|
10
|
+
require "action_dispatch/routing/endpoint"
|
|
11
|
+
|
|
12
|
+
module ActionDispatch
|
|
13
|
+
module Routing
|
|
14
|
+
class Mapper
|
|
15
|
+
class BacktraceCleaner < ActiveSupport::BacktraceCleaner # :nodoc:
|
|
16
|
+
def initialize
|
|
17
|
+
super
|
|
18
|
+
remove_silencers!
|
|
19
|
+
add_core_silencer
|
|
20
|
+
add_stdlib_silencer
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]
|
|
25
|
+
|
|
26
|
+
cattr_accessor :route_source_locations, instance_accessor: false, default: false
|
|
27
|
+
cattr_accessor :backtrace_cleaner, instance_accessor: false, default: BacktraceCleaner.new
|
|
28
|
+
|
|
29
|
+
class Constraints < Routing::Endpoint # :nodoc:
|
|
30
|
+
attr_reader :app, :constraints
|
|
31
|
+
|
|
32
|
+
SERVE = ->(app, req) { app.serve req }
|
|
33
|
+
CALL = ->(app, req) { app.call req.env }
|
|
34
|
+
|
|
35
|
+
def initialize(app, constraints, strategy)
|
|
36
|
+
# Unwrap Constraints objects. I don't actually think it's possible to pass a
|
|
37
|
+
# Constraints object to this constructor, but there were multiple places that
|
|
38
|
+
# kept testing children of this object. I **think** they were just being
|
|
39
|
+
# defensive, but I have no idea.
|
|
40
|
+
if app.is_a?(self.class)
|
|
41
|
+
constraints += app.constraints
|
|
42
|
+
app = app.app
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
@strategy = strategy
|
|
46
|
+
|
|
47
|
+
@app, @constraints, = app, constraints
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def dispatcher?; @strategy == SERVE; end
|
|
51
|
+
|
|
52
|
+
def matches?(req)
|
|
53
|
+
@constraints.all? do |constraint|
|
|
54
|
+
(constraint.respond_to?(:matches?) && constraint.matches?(req)) ||
|
|
55
|
+
(constraint.respond_to?(:call) && constraint.call(*constraint_args(constraint, req)))
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def serve(req)
|
|
60
|
+
return [ 404, { Constants::X_CASCADE => "pass" }, [] ] unless matches?(req)
|
|
61
|
+
|
|
62
|
+
@strategy.call @app, req
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
def constraint_args(constraint, request)
|
|
67
|
+
arity = if constraint.respond_to?(:arity)
|
|
68
|
+
constraint.arity
|
|
69
|
+
else
|
|
70
|
+
constraint.method(:call).arity
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if arity < 1
|
|
74
|
+
[]
|
|
75
|
+
elsif arity == 1
|
|
76
|
+
[request]
|
|
77
|
+
else
|
|
78
|
+
[request.path_parameters, request]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
class Mapping # :nodoc:
|
|
84
|
+
ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
|
|
85
|
+
OPTIONAL_FORMAT_REGEX = %r{(?:\(\.:format\)+|\.:format|/)\Z}
|
|
86
|
+
|
|
87
|
+
attr_reader :path, :requirements, :defaults, :to, :default_controller,
|
|
88
|
+
:default_action, :required_defaults, :ast, :scope_options
|
|
89
|
+
|
|
90
|
+
def self.build(scope, set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
|
|
91
|
+
scope_params = {
|
|
92
|
+
blocks: scope[:blocks] || [],
|
|
93
|
+
constraints: scope[:constraints] || {},
|
|
94
|
+
defaults: (scope[:defaults] || {}).dup,
|
|
95
|
+
module: scope[:module],
|
|
96
|
+
options: scope[:options] || {}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
new set: set, ast: ast, controller: controller, default_action: default_action,
|
|
100
|
+
to: to, formatted: formatted, via: via, options_constraints: options_constraints,
|
|
101
|
+
anchor: anchor, scope_params: scope_params, options: scope_params[:options].merge(options)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def self.check_via(via)
|
|
105
|
+
if via.empty?
|
|
106
|
+
msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
|
|
107
|
+
"If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \
|
|
108
|
+
"If you want to expose your action to GET, use `get` in the router:\n" \
|
|
109
|
+
" Instead of: match \"controller#action\"\n" \
|
|
110
|
+
" Do: get \"controller#action\""
|
|
111
|
+
raise ArgumentError, msg
|
|
112
|
+
end
|
|
113
|
+
via
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def self.normalize_path(path, format)
|
|
117
|
+
path = Mapper.normalize_path(path)
|
|
118
|
+
|
|
119
|
+
if format == true
|
|
120
|
+
"#{path}.:format"
|
|
121
|
+
elsif optional_format?(path, format)
|
|
122
|
+
"#{path}(.:format)"
|
|
123
|
+
else
|
|
124
|
+
path
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def self.optional_format?(path, format)
|
|
129
|
+
format != false && !path.match?(OPTIONAL_FORMAT_REGEX)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def initialize(set:, ast:, controller:, default_action:, to:, formatted:, via:, options_constraints:, anchor:, scope_params:, options:)
|
|
133
|
+
@defaults = scope_params[:defaults]
|
|
134
|
+
@set = set
|
|
135
|
+
@to = intern(to)
|
|
136
|
+
@default_controller = intern(controller)
|
|
137
|
+
@default_action = intern(default_action)
|
|
138
|
+
@anchor = anchor
|
|
139
|
+
@via = via
|
|
140
|
+
@internal = options.delete(:internal)
|
|
141
|
+
@scope_options = scope_params[:options]
|
|
142
|
+
ast = Journey::Ast.new(ast, formatted)
|
|
143
|
+
|
|
144
|
+
options = ast.wildcard_options.merge!(options)
|
|
145
|
+
|
|
146
|
+
options = normalize_options!(options, ast.path_params, scope_params[:module])
|
|
147
|
+
|
|
148
|
+
split_options = constraints(options, ast.path_params)
|
|
149
|
+
|
|
150
|
+
constraints = scope_params[:constraints].merge Hash[split_options[:constraints] || []]
|
|
151
|
+
|
|
152
|
+
if options_constraints.is_a?(Hash)
|
|
153
|
+
@defaults = Hash[options_constraints.find_all { |key, default|
|
|
154
|
+
URL_OPTIONS.include?(key) && (String === default || Integer === default)
|
|
155
|
+
}].merge @defaults
|
|
156
|
+
@blocks = scope_params[:blocks]
|
|
157
|
+
constraints.merge! options_constraints
|
|
158
|
+
else
|
|
159
|
+
@blocks = blocks(options_constraints)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
requirements, conditions = split_constraints ast.path_params, constraints
|
|
163
|
+
verify_regexp_requirements requirements, ast.wildcard_options
|
|
164
|
+
|
|
165
|
+
formats = normalize_format(formatted)
|
|
166
|
+
|
|
167
|
+
@requirements = formats[:requirements].merge Hash[requirements]
|
|
168
|
+
@conditions = Hash[conditions]
|
|
169
|
+
@defaults = formats[:defaults].merge(@defaults).merge(normalize_defaults(options))
|
|
170
|
+
|
|
171
|
+
if ast.path_params.include?(:action) && !@requirements.key?(:action)
|
|
172
|
+
@defaults[:action] ||= "index"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
@required_defaults = (split_options[:required_defaults] || []).map(&:first)
|
|
176
|
+
|
|
177
|
+
ast.requirements = @requirements
|
|
178
|
+
@path = Journey::Path::Pattern.new(ast, @requirements, JOINED_SEPARATORS, @anchor)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
JOINED_SEPARATORS = SEPARATORS.join # :nodoc:
|
|
182
|
+
|
|
183
|
+
def make_route(name, precedence)
|
|
184
|
+
Journey::Route.new(name: name, app: application, path: path, constraints: conditions,
|
|
185
|
+
required_defaults: required_defaults, defaults: defaults,
|
|
186
|
+
request_method_match: request_method, precedence: precedence,
|
|
187
|
+
scope_options: scope_options, internal: @internal, source_location: route_source_location)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def application
|
|
191
|
+
app(@blocks)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def conditions
|
|
195
|
+
build_conditions @conditions, @set.request_class
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def build_conditions(current_conditions, request_class)
|
|
199
|
+
conditions = current_conditions.dup
|
|
200
|
+
|
|
201
|
+
conditions.keep_if do |k, _|
|
|
202
|
+
request_class.public_method_defined?(k)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
private :build_conditions
|
|
206
|
+
|
|
207
|
+
def request_method
|
|
208
|
+
@via.map { |x| Journey::Route.verb_matcher(x) }
|
|
209
|
+
end
|
|
210
|
+
private :request_method
|
|
211
|
+
|
|
212
|
+
private
|
|
213
|
+
def intern(object)
|
|
214
|
+
object.is_a?(String) ? -object : object
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def normalize_options!(options, path_params, modyoule)
|
|
218
|
+
if path_params.include?(:controller)
|
|
219
|
+
raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule
|
|
220
|
+
|
|
221
|
+
# Add a default constraint for :controller path segments that matches namespaced
|
|
222
|
+
# controllers with default routes like :controller/:action/:id(.:format), e.g:
|
|
223
|
+
# GET /admin/products/show/1
|
|
224
|
+
# # > { controller: 'admin/products', action: 'show', id: '1' }
|
|
225
|
+
options[:controller] ||= /.+?/
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
if to.respond_to?(:action) || to.respond_to?(:call)
|
|
229
|
+
options
|
|
230
|
+
else
|
|
231
|
+
if to.nil?
|
|
232
|
+
controller = default_controller
|
|
233
|
+
action = default_action
|
|
234
|
+
elsif to.is_a?(String)
|
|
235
|
+
if to.include?("#")
|
|
236
|
+
to_endpoint = to.split("#").map!(&:-@)
|
|
237
|
+
controller = to_endpoint[0]
|
|
238
|
+
action = to_endpoint[1]
|
|
239
|
+
else
|
|
240
|
+
controller = default_controller
|
|
241
|
+
action = to
|
|
242
|
+
end
|
|
243
|
+
else
|
|
244
|
+
raise ArgumentError, ":to must respond to `action` or `call`, or it must be a String that includes '#', or the controller should be implicit"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
controller = add_controller_module(controller, modyoule)
|
|
248
|
+
|
|
249
|
+
options.merge! check_controller_and_action(path_params, controller, action)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def split_constraints(path_params, constraints)
|
|
254
|
+
constraints.partition do |key, requirement|
|
|
255
|
+
path_params.include?(key) || key == :controller
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def normalize_format(formatted)
|
|
260
|
+
case formatted
|
|
261
|
+
when true
|
|
262
|
+
{ requirements: { format: /.+/ },
|
|
263
|
+
defaults: {} }
|
|
264
|
+
when Regexp
|
|
265
|
+
{ requirements: { format: formatted },
|
|
266
|
+
defaults: { format: nil } }
|
|
267
|
+
when String
|
|
268
|
+
{ requirements: { format: Regexp.compile(formatted) },
|
|
269
|
+
defaults: { format: formatted } }
|
|
270
|
+
else
|
|
271
|
+
{ requirements: {}, defaults: {} }
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def verify_regexp_requirements(requirements, wildcard_options)
|
|
276
|
+
requirements.each do |requirement, regex|
|
|
277
|
+
next unless regex.is_a? Regexp
|
|
278
|
+
|
|
279
|
+
if ANCHOR_CHARACTERS_REGEX.match?(regex.source)
|
|
280
|
+
raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
if regex.multiline?
|
|
284
|
+
next if wildcard_options.key?(requirement)
|
|
285
|
+
|
|
286
|
+
raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{regex.inspect}"
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def normalize_defaults(options)
|
|
292
|
+
Hash[options.reject { |_, default| Regexp === default }]
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def app(blocks)
|
|
296
|
+
if to.respond_to?(:action)
|
|
297
|
+
Routing::RouteSet::StaticDispatcher.new to
|
|
298
|
+
elsif to.respond_to?(:call)
|
|
299
|
+
Constraints.new(to, blocks, Constraints::CALL)
|
|
300
|
+
elsif blocks.any?
|
|
301
|
+
Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE)
|
|
302
|
+
else
|
|
303
|
+
dispatcher(defaults.key?(:controller))
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def check_controller_and_action(path_params, controller, action)
|
|
308
|
+
hash = check_part(:controller, controller, path_params, {}) do |part|
|
|
309
|
+
translate_controller(part) {
|
|
310
|
+
message = +"'#{part}' is not a supported controller name. This can lead to potential routing problems."
|
|
311
|
+
message << " See https://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
|
|
312
|
+
|
|
313
|
+
raise ArgumentError, message
|
|
314
|
+
}
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
check_part(:action, action, path_params, hash) { |part|
|
|
318
|
+
part.is_a?(Regexp) ? part : part.to_s
|
|
319
|
+
}
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def check_part(name, part, path_params, hash)
|
|
323
|
+
if part
|
|
324
|
+
hash[name] = yield(part)
|
|
325
|
+
else
|
|
326
|
+
unless path_params.include?(name)
|
|
327
|
+
message = "Missing :#{name} key on routes definition, please check your routes."
|
|
328
|
+
raise ArgumentError, message
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
hash
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def add_controller_module(controller, modyoule)
|
|
335
|
+
if modyoule && !controller.is_a?(Regexp)
|
|
336
|
+
if controller&.start_with?("/")
|
|
337
|
+
-controller[1..-1]
|
|
338
|
+
else
|
|
339
|
+
-[modyoule, controller].compact.join("/")
|
|
340
|
+
end
|
|
341
|
+
else
|
|
342
|
+
controller
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def translate_controller(controller)
|
|
347
|
+
return controller if Regexp === controller
|
|
348
|
+
return controller.to_s if /\A[a-z_0-9][a-z_0-9\/]*\z/.match?(controller)
|
|
349
|
+
|
|
350
|
+
yield
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def blocks(callable_constraint)
|
|
354
|
+
unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?)
|
|
355
|
+
raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?"
|
|
356
|
+
end
|
|
357
|
+
[callable_constraint]
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def constraints(options, path_params)
|
|
361
|
+
options.group_by do |key, option|
|
|
362
|
+
if Regexp === option
|
|
363
|
+
:constraints
|
|
364
|
+
else
|
|
365
|
+
if path_params.include?(key)
|
|
366
|
+
:path_params
|
|
367
|
+
else
|
|
368
|
+
:required_defaults
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def dispatcher(raise_on_name_error)
|
|
375
|
+
Routing::RouteSet::Dispatcher.new raise_on_name_error
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
if Thread.respond_to?(:each_caller_location)
|
|
379
|
+
def route_source_location
|
|
380
|
+
if Mapper.route_source_locations
|
|
381
|
+
action_dispatch_dir = File.expand_path("..", __dir__)
|
|
382
|
+
Thread.each_caller_location do |location|
|
|
383
|
+
next if location.path.start_with?(action_dispatch_dir)
|
|
384
|
+
|
|
385
|
+
cleaned_path = Mapper.backtrace_cleaner.clean_frame(location.path)
|
|
386
|
+
next if cleaned_path.nil?
|
|
387
|
+
|
|
388
|
+
return "#{cleaned_path}:#{location.lineno}"
|
|
389
|
+
end
|
|
390
|
+
nil
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
else
|
|
394
|
+
def route_source_location
|
|
395
|
+
if Mapper.route_source_locations
|
|
396
|
+
action_dispatch_dir = File.expand_path("..", __dir__)
|
|
397
|
+
caller_locations.each do |location|
|
|
398
|
+
next if location.path.start_with?(action_dispatch_dir)
|
|
399
|
+
|
|
400
|
+
cleaned_path = Mapper.backtrace_cleaner.clean_frame(location.path)
|
|
401
|
+
next if cleaned_path.nil?
|
|
402
|
+
|
|
403
|
+
return "#{cleaned_path}:#{location.lineno}"
|
|
404
|
+
end
|
|
405
|
+
nil
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Invokes Journey::Router::Utils.normalize_path, then ensures that /(:locale)
|
|
412
|
+
# becomes (/:locale). Except for root cases, where the former is the correct
|
|
413
|
+
# one.
|
|
414
|
+
def self.normalize_path(path)
|
|
415
|
+
path = Journey::Router::Utils.normalize_path(path)
|
|
416
|
+
|
|
417
|
+
# the path for a root URL at this point can be something like
|
|
418
|
+
# "/(/:locale)(/:platform)/(:browser)", and we would want
|
|
419
|
+
# "/(:locale)(/:platform)(/:browser)" reverse "/(", "/((" etc to "(/", "((/" etc
|
|
420
|
+
path.gsub!(%r{/(\(+)/?}, '\1/')
|
|
421
|
+
# if a path is all optional segments, change the leading "(/" back to "/(" so it
|
|
422
|
+
# evaluates to "/" when interpreted with no options. Unless, however, at least
|
|
423
|
+
# one secondary segment consists of a static part, ex.
|
|
424
|
+
# "(/:locale)(/pages/:page)"
|
|
425
|
+
path.sub!(%r{^(\(+)/}, '/\1') if %r{^(\(+[^)]+\))(\(+/:[^)]+\))*$}.match?(path)
|
|
426
|
+
path
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def self.normalize_name(name)
|
|
430
|
+
normalize_path(name)[1..-1].tr("/", "_")
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
module Base
|
|
434
|
+
# Matches a URL pattern to one or more routes.
|
|
435
|
+
#
|
|
436
|
+
# You should not use the `match` method in your router without specifying an
|
|
437
|
+
# HTTP method.
|
|
438
|
+
#
|
|
439
|
+
# If you want to expose your action to both GET and POST, use:
|
|
440
|
+
#
|
|
441
|
+
# # sets :controller, :action, and :id in params
|
|
442
|
+
# match ':controller/:action/:id', via: [:get, :post]
|
|
443
|
+
#
|
|
444
|
+
# Note that `:controller`, `:action`, and `:id` are interpreted as URL query
|
|
445
|
+
# parameters and thus available through `params` in an action.
|
|
446
|
+
#
|
|
447
|
+
# If you want to expose your action to GET, use `get` in the router:
|
|
448
|
+
#
|
|
449
|
+
# Instead of:
|
|
450
|
+
#
|
|
451
|
+
# match ":controller/:action/:id"
|
|
452
|
+
#
|
|
453
|
+
# Do:
|
|
454
|
+
#
|
|
455
|
+
# get ":controller/:action/:id"
|
|
456
|
+
#
|
|
457
|
+
# Two of these symbols are special, `:controller` maps to the controller and
|
|
458
|
+
# `:action` to the controller's action. A pattern can also map wildcard segments
|
|
459
|
+
# (globs) to params:
|
|
460
|
+
#
|
|
461
|
+
# get 'songs/*category/:title', to: 'songs#show'
|
|
462
|
+
#
|
|
463
|
+
# # 'songs/rock/classic/stairway-to-heaven' sets
|
|
464
|
+
# # params[:category] = 'rock/classic'
|
|
465
|
+
# # params[:title] = 'stairway-to-heaven'
|
|
466
|
+
#
|
|
467
|
+
# To match a wildcard parameter, it must have a name assigned to it. Without a
|
|
468
|
+
# variable name to attach the glob parameter to, the route can't be parsed.
|
|
469
|
+
#
|
|
470
|
+
# When a pattern points to an internal route, the route's `:action` and
|
|
471
|
+
# `:controller` should be set in options or hash shorthand. Examples:
|
|
472
|
+
#
|
|
473
|
+
# match 'photos/:id', to: 'photos#show', via: :get
|
|
474
|
+
# match 'photos/:id', controller: 'photos', action: 'show', via: :get
|
|
475
|
+
#
|
|
476
|
+
# A pattern can also point to a `Rack` endpoint i.e. anything that responds to
|
|
477
|
+
# `call`:
|
|
478
|
+
#
|
|
479
|
+
# match 'photos/:id', to: -> (hash) { [200, {}, ["Coming soon"]] }, via: :get
|
|
480
|
+
# match 'photos/:id', to: PhotoRackApp, via: :get
|
|
481
|
+
# # Yes, controller actions are just rack endpoints
|
|
482
|
+
# match 'photos/:id', to: PhotosController.action(:show), via: :get
|
|
483
|
+
#
|
|
484
|
+
# Because requesting various HTTP verbs with a single action has security
|
|
485
|
+
# implications, you must either specify the actions in the via options or use
|
|
486
|
+
# one of the [HttpHelpers](rdoc-ref:HttpHelpers) instead `match`
|
|
487
|
+
#
|
|
488
|
+
# ### Options
|
|
489
|
+
#
|
|
490
|
+
# Any options not seen here are passed on as params with the URL.
|
|
491
|
+
#
|
|
492
|
+
# :controller
|
|
493
|
+
# : The route's controller.
|
|
494
|
+
#
|
|
495
|
+
# :action
|
|
496
|
+
# : The route's action.
|
|
497
|
+
#
|
|
498
|
+
# :param
|
|
499
|
+
# : Overrides the default resource identifier `:id` (name of the dynamic
|
|
500
|
+
# segment used to generate the routes). You can access that segment from
|
|
501
|
+
# your controller using `params[<:param>]`. In your router:
|
|
502
|
+
#
|
|
503
|
+
# resources :users, param: :name
|
|
504
|
+
#
|
|
505
|
+
# The `users` resource here will have the following routes generated for it:
|
|
506
|
+
#
|
|
507
|
+
# GET /users(.:format)
|
|
508
|
+
# POST /users(.:format)
|
|
509
|
+
# GET /users/new(.:format)
|
|
510
|
+
# GET /users/:name/edit(.:format)
|
|
511
|
+
# GET /users/:name(.:format)
|
|
512
|
+
# PATCH/PUT /users/:name(.:format)
|
|
513
|
+
# DELETE /users/:name(.:format)
|
|
514
|
+
#
|
|
515
|
+
# You can override `ActiveRecord::Base#to_param` of a related model to
|
|
516
|
+
# construct a URL:
|
|
517
|
+
#
|
|
518
|
+
# class User < ActiveRecord::Base
|
|
519
|
+
# def to_param
|
|
520
|
+
# name
|
|
521
|
+
# end
|
|
522
|
+
# end
|
|
523
|
+
#
|
|
524
|
+
# user = User.find_by(name: 'Phusion')
|
|
525
|
+
# user_path(user) # => "/users/Phusion"
|
|
526
|
+
#
|
|
527
|
+
# :path
|
|
528
|
+
# : The path prefix for the routes.
|
|
529
|
+
#
|
|
530
|
+
# :module
|
|
531
|
+
# : The namespace for :controller.
|
|
532
|
+
#
|
|
533
|
+
# match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: :get
|
|
534
|
+
# # => Sekret::PostsController
|
|
535
|
+
#
|
|
536
|
+
# See `Scoping#namespace` for its scope equivalent.
|
|
537
|
+
#
|
|
538
|
+
# :as
|
|
539
|
+
# : The name used to generate routing helpers.
|
|
540
|
+
#
|
|
541
|
+
# :via
|
|
542
|
+
# : Allowed HTTP verb(s) for route.
|
|
543
|
+
#
|
|
544
|
+
# match 'path', to: 'c#a', via: :get
|
|
545
|
+
# match 'path', to: 'c#a', via: [:get, :post]
|
|
546
|
+
# match 'path', to: 'c#a', via: :all
|
|
547
|
+
#
|
|
548
|
+
# :to
|
|
549
|
+
# : Points to a `Rack` endpoint. Can be an object that responds to `call` or a
|
|
550
|
+
# string representing a controller's action.
|
|
551
|
+
#
|
|
552
|
+
# match 'path', to: 'controller#action', via: :get
|
|
553
|
+
# match 'path', to: -> (env) { [200, {}, ["Success!"]] }, via: :get
|
|
554
|
+
# match 'path', to: RackApp, via: :get
|
|
555
|
+
#
|
|
556
|
+
# :on
|
|
557
|
+
# : Shorthand for wrapping routes in a specific RESTful context. Valid values
|
|
558
|
+
# are `:member`, `:collection`, and `:new`. Only use within `resource(s)`
|
|
559
|
+
# block. For example:
|
|
560
|
+
#
|
|
561
|
+
# resource :bar do
|
|
562
|
+
# match 'foo', to: 'c#a', on: :member, via: [:get, :post]
|
|
563
|
+
# end
|
|
564
|
+
#
|
|
565
|
+
# Is equivalent to:
|
|
566
|
+
#
|
|
567
|
+
# resource :bar do
|
|
568
|
+
# member do
|
|
569
|
+
# match 'foo', to: 'c#a', via: [:get, :post]
|
|
570
|
+
# end
|
|
571
|
+
# end
|
|
572
|
+
#
|
|
573
|
+
# :constraints
|
|
574
|
+
# : Constrains parameters with a hash of regular expressions or an object that
|
|
575
|
+
# responds to `matches?`. In addition, constraints other than path can also
|
|
576
|
+
# be specified with any object that responds to `===` (e.g. String, Array,
|
|
577
|
+
# Range, etc.).
|
|
578
|
+
#
|
|
579
|
+
# match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }, via: :get
|
|
580
|
+
#
|
|
581
|
+
# match 'json_only', constraints: { format: 'json' }, via: :get
|
|
582
|
+
#
|
|
583
|
+
# class PermitList
|
|
584
|
+
# def matches?(request) request.remote_ip == '1.2.3.4' end
|
|
585
|
+
# end
|
|
586
|
+
# match 'path', to: 'c#a', constraints: PermitList.new, via: :get
|
|
587
|
+
#
|
|
588
|
+
# See `Scoping#constraints` for more examples with its scope equivalent.
|
|
589
|
+
#
|
|
590
|
+
# :defaults
|
|
591
|
+
# : Sets defaults for parameters
|
|
592
|
+
#
|
|
593
|
+
# # Sets params[:format] to 'jpg' by default
|
|
594
|
+
# match 'path', to: 'c#a', defaults: { format: 'jpg' }, via: :get
|
|
595
|
+
#
|
|
596
|
+
# See `Scoping#defaults` for its scope equivalent.
|
|
597
|
+
#
|
|
598
|
+
# :anchor
|
|
599
|
+
# : Boolean to anchor a `match` pattern. Default is true. When set to false,
|
|
600
|
+
# the pattern matches any request prefixed with the given path.
|
|
601
|
+
#
|
|
602
|
+
# # Matches any request starting with 'path'
|
|
603
|
+
# match 'path', to: 'c#a', anchor: false, via: :get
|
|
604
|
+
#
|
|
605
|
+
# :format
|
|
606
|
+
# : Allows you to specify the default value for optional `format` segment or
|
|
607
|
+
# disable it by supplying `false`.
|
|
608
|
+
#
|
|
609
|
+
def match(path, options = nil)
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
# Mount a Rack-based application to be used within the application.
|
|
613
|
+
#
|
|
614
|
+
# mount SomeRackApp, at: "some_route"
|
|
615
|
+
#
|
|
616
|
+
# For options, see `match`, as `mount` uses it internally.
|
|
617
|
+
#
|
|
618
|
+
# All mounted applications come with routing helpers to access them. These are
|
|
619
|
+
# named after the class specified, so for the above example the helper is either
|
|
620
|
+
# `some_rack_app_path` or `some_rack_app_url`. To customize this helper's name,
|
|
621
|
+
# use the `:as` option:
|
|
622
|
+
#
|
|
623
|
+
# mount(SomeRackApp, at: "some_route", as: "exciting")
|
|
624
|
+
#
|
|
625
|
+
# This will generate the `exciting_path` and `exciting_url` helpers which can be
|
|
626
|
+
# used to navigate to this mounted app.
|
|
627
|
+
def mount(app, options = nil)
|
|
628
|
+
if options
|
|
629
|
+
path = options.delete(:at)
|
|
630
|
+
elsif Hash === app
|
|
631
|
+
options = app
|
|
632
|
+
app, path = options.find { |k, _| k.respond_to?(:call) }
|
|
633
|
+
options.delete(app) if app
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
raise ArgumentError, "A rack application must be specified" unless app.respond_to?(:call)
|
|
637
|
+
raise ArgumentError, <<~MSG unless path
|
|
638
|
+
Must be called with mount point
|
|
639
|
+
|
|
640
|
+
mount SomeRackApp, at: "some_route"
|
|
641
|
+
or
|
|
642
|
+
mount(SomeRackApp => "some_route")
|
|
643
|
+
MSG
|
|
644
|
+
|
|
645
|
+
rails_app = rails_app? app
|
|
646
|
+
options[:as] ||= app_name(app, rails_app)
|
|
647
|
+
|
|
648
|
+
target_as = name_for_action(options[:as], path)
|
|
649
|
+
options[:via] ||= :all
|
|
650
|
+
|
|
651
|
+
match(path, { to: app, anchor: false, format: false }.merge(options))
|
|
652
|
+
|
|
653
|
+
define_generate_prefix(app, target_as) if rails_app
|
|
654
|
+
self
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
def default_url_options=(options)
|
|
658
|
+
@set.default_url_options = options
|
|
659
|
+
end
|
|
660
|
+
alias_method :default_url_options, :default_url_options=
|
|
661
|
+
|
|
662
|
+
def with_default_scope(scope, &block)
|
|
663
|
+
scope(scope) do
|
|
664
|
+
instance_exec(&block)
|
|
665
|
+
end
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
# Query if the following named route was already defined.
|
|
669
|
+
def has_named_route?(name)
|
|
670
|
+
@set.named_routes.key?(name)
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
private
|
|
674
|
+
def rails_app?(app)
|
|
675
|
+
app.is_a?(Class) && app < Rails::Railtie
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
def app_name(app, rails_app)
|
|
679
|
+
if rails_app
|
|
680
|
+
app.railtie_name
|
|
681
|
+
elsif app.is_a?(Class)
|
|
682
|
+
class_name = app.name
|
|
683
|
+
ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
|
|
684
|
+
end
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
def define_generate_prefix(app, name)
|
|
688
|
+
_route = @set.named_routes.get name
|
|
689
|
+
_routes = @set
|
|
690
|
+
_url_helpers = @set.url_helpers
|
|
691
|
+
|
|
692
|
+
script_namer = ->(options) do
|
|
693
|
+
prefix_options = options.slice(*_route.segment_keys)
|
|
694
|
+
prefix_options[:script_name] = "" if options[:original_script_name]
|
|
695
|
+
|
|
696
|
+
if options[:_recall]
|
|
697
|
+
prefix_options.reverse_merge!(options[:_recall].slice(*_route.segment_keys))
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
# We must actually delete prefix segment keys to avoid passing them to next
|
|
701
|
+
# url_for.
|
|
702
|
+
_route.segment_keys.each { |k| options.delete(k) }
|
|
703
|
+
_url_helpers.public_send("#{name}_path", prefix_options)
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
app.routes.define_mounted_helper(name, script_namer)
|
|
707
|
+
|
|
708
|
+
app.routes.extend Module.new {
|
|
709
|
+
def optimize_routes_generation?; false; end
|
|
710
|
+
|
|
711
|
+
define_method :find_script_name do |options|
|
|
712
|
+
if options.key?(:script_name) && options[:script_name].present?
|
|
713
|
+
super(options)
|
|
714
|
+
else
|
|
715
|
+
script_namer.call(options)
|
|
716
|
+
end
|
|
717
|
+
end
|
|
718
|
+
}
|
|
719
|
+
end
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
module HttpHelpers
|
|
723
|
+
# Define a route that only recognizes HTTP GET. For supported arguments, see
|
|
724
|
+
# [match](rdoc-ref:Base#match)
|
|
725
|
+
#
|
|
726
|
+
# get 'bacon', to: 'food#bacon'
|
|
727
|
+
def get(*args, &block)
|
|
728
|
+
map_method(:get, args, &block)
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
# Define a route that only recognizes HTTP POST. For supported arguments, see
|
|
732
|
+
# [match](rdoc-ref:Base#match)
|
|
733
|
+
#
|
|
734
|
+
# post 'bacon', to: 'food#bacon'
|
|
735
|
+
def post(*args, &block)
|
|
736
|
+
map_method(:post, args, &block)
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
# Define a route that only recognizes HTTP PATCH. For supported arguments, see
|
|
740
|
+
# [match](rdoc-ref:Base#match)
|
|
741
|
+
#
|
|
742
|
+
# patch 'bacon', to: 'food#bacon'
|
|
743
|
+
def patch(*args, &block)
|
|
744
|
+
map_method(:patch, args, &block)
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
# Define a route that only recognizes HTTP PUT. For supported arguments, see
|
|
748
|
+
# [match](rdoc-ref:Base#match)
|
|
749
|
+
#
|
|
750
|
+
# put 'bacon', to: 'food#bacon'
|
|
751
|
+
def put(*args, &block)
|
|
752
|
+
map_method(:put, args, &block)
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
# Define a route that only recognizes HTTP DELETE. For supported arguments, see
|
|
756
|
+
# [match](rdoc-ref:Base#match)
|
|
757
|
+
#
|
|
758
|
+
# delete 'broccoli', to: 'food#broccoli'
|
|
759
|
+
def delete(*args, &block)
|
|
760
|
+
map_method(:delete, args, &block)
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
# Define a route that only recognizes HTTP OPTIONS. For supported arguments, see
|
|
764
|
+
# [match](rdoc-ref:Base#match)
|
|
765
|
+
#
|
|
766
|
+
# options 'carrots', to: 'food#carrots'
|
|
767
|
+
def options(*args, &block)
|
|
768
|
+
map_method(:options, args, &block)
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
# Define a route that recognizes HTTP CONNECT (and GET) requests. More
|
|
772
|
+
# specifically this recognizes HTTP/1 protocol upgrade requests and HTTP/2
|
|
773
|
+
# CONNECT requests with the protocol pseudo header. For supported arguments,
|
|
774
|
+
# see [match](rdoc-ref:Base#match)
|
|
775
|
+
#
|
|
776
|
+
# connect 'live', to: 'live#index'
|
|
777
|
+
def connect(*args, &block)
|
|
778
|
+
map_method([:get, :connect], args, &block)
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
private
|
|
782
|
+
def map_method(method, args, &block)
|
|
783
|
+
options = args.extract_options!
|
|
784
|
+
options[:via] = method
|
|
785
|
+
match(*args, options, &block)
|
|
786
|
+
self
|
|
787
|
+
end
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
# You may wish to organize groups of controllers under a namespace. Most
|
|
791
|
+
# commonly, you might group a number of administrative controllers under an
|
|
792
|
+
# `admin` namespace. You would place these controllers under the
|
|
793
|
+
# `app/controllers/admin` directory, and you can group them together in your
|
|
794
|
+
# router:
|
|
795
|
+
#
|
|
796
|
+
# namespace "admin" do
|
|
797
|
+
# resources :posts, :comments
|
|
798
|
+
# end
|
|
799
|
+
#
|
|
800
|
+
# This will create a number of routes for each of the posts and comments
|
|
801
|
+
# controller. For `Admin::PostsController`, Rails will create:
|
|
802
|
+
#
|
|
803
|
+
# GET /admin/posts
|
|
804
|
+
# GET /admin/posts/new
|
|
805
|
+
# POST /admin/posts
|
|
806
|
+
# GET /admin/posts/1
|
|
807
|
+
# GET /admin/posts/1/edit
|
|
808
|
+
# PATCH/PUT /admin/posts/1
|
|
809
|
+
# DELETE /admin/posts/1
|
|
810
|
+
#
|
|
811
|
+
# If you want to route /posts (without the prefix /admin) to
|
|
812
|
+
# `Admin::PostsController`, you could use
|
|
813
|
+
#
|
|
814
|
+
# scope module: "admin" do
|
|
815
|
+
# resources :posts
|
|
816
|
+
# end
|
|
817
|
+
#
|
|
818
|
+
# or, for a single case
|
|
819
|
+
#
|
|
820
|
+
# resources :posts, module: "admin"
|
|
821
|
+
#
|
|
822
|
+
# If you want to route /admin/posts to `PostsController` (without the `Admin::`
|
|
823
|
+
# module prefix), you could use
|
|
824
|
+
#
|
|
825
|
+
# scope "/admin" do
|
|
826
|
+
# resources :posts
|
|
827
|
+
# end
|
|
828
|
+
#
|
|
829
|
+
# or, for a single case
|
|
830
|
+
#
|
|
831
|
+
# resources :posts, path: "/admin/posts"
|
|
832
|
+
#
|
|
833
|
+
# In each of these cases, the named routes remain the same as if you did not use
|
|
834
|
+
# scope. In the last case, the following paths map to `PostsController`:
|
|
835
|
+
#
|
|
836
|
+
# GET /admin/posts
|
|
837
|
+
# GET /admin/posts/new
|
|
838
|
+
# POST /admin/posts
|
|
839
|
+
# GET /admin/posts/1
|
|
840
|
+
# GET /admin/posts/1/edit
|
|
841
|
+
# PATCH/PUT /admin/posts/1
|
|
842
|
+
# DELETE /admin/posts/1
|
|
843
|
+
module Scoping
|
|
844
|
+
# Scopes a set of routes to the given default options.
|
|
845
|
+
#
|
|
846
|
+
# Take the following route definition as an example:
|
|
847
|
+
#
|
|
848
|
+
# scope path: ":account_id", as: "account" do
|
|
849
|
+
# resources :projects
|
|
850
|
+
# end
|
|
851
|
+
#
|
|
852
|
+
# This generates helpers such as `account_projects_path`, just like `resources`
|
|
853
|
+
# does. The difference here being that the routes generated are like
|
|
854
|
+
# /:account_id/projects, rather than /accounts/:account_id/projects.
|
|
855
|
+
#
|
|
856
|
+
# ### Options
|
|
857
|
+
#
|
|
858
|
+
# Takes same options as `Base#match` and `Resources#resources`.
|
|
859
|
+
#
|
|
860
|
+
# # route /posts (without the prefix /admin) to +Admin::PostsController+
|
|
861
|
+
# scope module: "admin" do
|
|
862
|
+
# resources :posts
|
|
863
|
+
# end
|
|
864
|
+
#
|
|
865
|
+
# # prefix the posts resource's requests with '/admin'
|
|
866
|
+
# scope path: "/admin" do
|
|
867
|
+
# resources :posts
|
|
868
|
+
# end
|
|
869
|
+
#
|
|
870
|
+
# # prefix the routing helper name: +sekret_posts_path+ instead of +posts_path+
|
|
871
|
+
# scope as: "sekret" do
|
|
872
|
+
# resources :posts
|
|
873
|
+
# end
|
|
874
|
+
def scope(*args)
|
|
875
|
+
options = args.extract_options!.dup
|
|
876
|
+
scope = {}
|
|
877
|
+
|
|
878
|
+
options[:path] = args.flatten.join("/") if args.any?
|
|
879
|
+
options[:constraints] ||= {}
|
|
880
|
+
|
|
881
|
+
unless nested_scope?
|
|
882
|
+
options[:shallow_path] ||= options[:path] if options.key?(:path)
|
|
883
|
+
options[:shallow_prefix] ||= options[:as] if options.key?(:as)
|
|
884
|
+
end
|
|
885
|
+
|
|
886
|
+
if options[:constraints].is_a?(Hash)
|
|
887
|
+
defaults = options[:constraints].select do |k, v|
|
|
888
|
+
URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Integer))
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
options[:defaults] = defaults.merge(options[:defaults] || {})
|
|
892
|
+
else
|
|
893
|
+
block, options[:constraints] = options[:constraints], {}
|
|
894
|
+
end
|
|
895
|
+
|
|
896
|
+
if options.key?(:only) || options.key?(:except)
|
|
897
|
+
scope[:action_options] = { only: options.delete(:only),
|
|
898
|
+
except: options.delete(:except) }
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
if options.key? :anchor
|
|
902
|
+
raise ArgumentError, "anchor is ignored unless passed to `match`"
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
@scope.options.each do |option|
|
|
906
|
+
if option == :blocks
|
|
907
|
+
value = block
|
|
908
|
+
elsif option == :options
|
|
909
|
+
value = options
|
|
910
|
+
else
|
|
911
|
+
value = options.delete(option) { POISON }
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
unless POISON == value
|
|
915
|
+
scope[option] = send("merge_#{option}_scope", @scope[option], value)
|
|
916
|
+
end
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
@scope = @scope.new scope
|
|
920
|
+
yield
|
|
921
|
+
self
|
|
922
|
+
ensure
|
|
923
|
+
@scope = @scope.parent
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
POISON = Object.new # :nodoc:
|
|
927
|
+
|
|
928
|
+
# Scopes routes to a specific controller
|
|
929
|
+
#
|
|
930
|
+
# controller "food" do
|
|
931
|
+
# match "bacon", action: :bacon, via: :get
|
|
932
|
+
# end
|
|
933
|
+
def controller(controller)
|
|
934
|
+
@scope = @scope.new(controller: controller)
|
|
935
|
+
yield
|
|
936
|
+
ensure
|
|
937
|
+
@scope = @scope.parent
|
|
938
|
+
end
|
|
939
|
+
|
|
940
|
+
# Scopes routes to a specific namespace. For example:
|
|
941
|
+
#
|
|
942
|
+
# namespace :admin do
|
|
943
|
+
# resources :posts
|
|
944
|
+
# end
|
|
945
|
+
#
|
|
946
|
+
# This generates the following routes:
|
|
947
|
+
#
|
|
948
|
+
# admin_posts GET /admin/posts(.:format) admin/posts#index
|
|
949
|
+
# admin_posts POST /admin/posts(.:format) admin/posts#create
|
|
950
|
+
# new_admin_post GET /admin/posts/new(.:format) admin/posts#new
|
|
951
|
+
# edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit
|
|
952
|
+
# admin_post GET /admin/posts/:id(.:format) admin/posts#show
|
|
953
|
+
# admin_post PATCH/PUT /admin/posts/:id(.:format) admin/posts#update
|
|
954
|
+
# admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy
|
|
955
|
+
#
|
|
956
|
+
# ### Options
|
|
957
|
+
#
|
|
958
|
+
# The `:path`, `:as`, `:module`, `:shallow_path`, and `:shallow_prefix` options
|
|
959
|
+
# all default to the name of the namespace.
|
|
960
|
+
#
|
|
961
|
+
# For options, see `Base#match`. For `:shallow_path` option, see
|
|
962
|
+
# `Resources#resources`.
|
|
963
|
+
#
|
|
964
|
+
# # accessible through /sekret/posts rather than /admin/posts
|
|
965
|
+
# namespace :admin, path: "sekret" do
|
|
966
|
+
# resources :posts
|
|
967
|
+
# end
|
|
968
|
+
#
|
|
969
|
+
# # maps to +Sekret::PostsController+ rather than +Admin::PostsController+
|
|
970
|
+
# namespace :admin, module: "sekret" do
|
|
971
|
+
# resources :posts
|
|
972
|
+
# end
|
|
973
|
+
#
|
|
974
|
+
# # generates +sekret_posts_path+ rather than +admin_posts_path+
|
|
975
|
+
# namespace :admin, as: "sekret" do
|
|
976
|
+
# resources :posts
|
|
977
|
+
# end
|
|
978
|
+
def namespace(path, options = {}, &block)
|
|
979
|
+
path = path.to_s
|
|
980
|
+
|
|
981
|
+
defaults = {
|
|
982
|
+
module: path,
|
|
983
|
+
as: options.fetch(:as, path),
|
|
984
|
+
shallow_path: options.fetch(:path, path),
|
|
985
|
+
shallow_prefix: options.fetch(:as, path)
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
path_scope(options.delete(:path) { path }) do
|
|
989
|
+
scope(defaults.merge!(options), &block)
|
|
990
|
+
end
|
|
991
|
+
end
|
|
992
|
+
|
|
993
|
+
# ### Parameter Restriction
|
|
994
|
+
# Allows you to constrain the nested routes based on a set of rules. For
|
|
995
|
+
# instance, in order to change the routes to allow for a dot character in the
|
|
996
|
+
# `id` parameter:
|
|
997
|
+
#
|
|
998
|
+
# constraints(id: /\d+\.\d+/) do
|
|
999
|
+
# resources :posts
|
|
1000
|
+
# end
|
|
1001
|
+
#
|
|
1002
|
+
# Now routes such as `/posts/1` will no longer be valid, but `/posts/1.1` will
|
|
1003
|
+
# be. The `id` parameter must match the constraint passed in for this example.
|
|
1004
|
+
#
|
|
1005
|
+
# You may use this to also restrict other parameters:
|
|
1006
|
+
#
|
|
1007
|
+
# resources :posts do
|
|
1008
|
+
# constraints(post_id: /\d+\.\d+/) do
|
|
1009
|
+
# resources :comments
|
|
1010
|
+
# end
|
|
1011
|
+
# end
|
|
1012
|
+
#
|
|
1013
|
+
# ### Restricting based on IP
|
|
1014
|
+
#
|
|
1015
|
+
# Routes can also be constrained to an IP or a certain range of IP addresses:
|
|
1016
|
+
#
|
|
1017
|
+
# constraints(ip: /192\.168\.\d+\.\d+/) do
|
|
1018
|
+
# resources :posts
|
|
1019
|
+
# end
|
|
1020
|
+
#
|
|
1021
|
+
# Any user connecting from the 192.168.* range will be able to see this
|
|
1022
|
+
# resource, where as any user connecting outside of this range will be told
|
|
1023
|
+
# there is no such route.
|
|
1024
|
+
#
|
|
1025
|
+
# ### Dynamic request matching
|
|
1026
|
+
#
|
|
1027
|
+
# Requests to routes can be constrained based on specific criteria:
|
|
1028
|
+
#
|
|
1029
|
+
# constraints(-> (req) { /iPhone/.match?(req.env["HTTP_USER_AGENT"]) }) do
|
|
1030
|
+
# resources :iphones
|
|
1031
|
+
# end
|
|
1032
|
+
#
|
|
1033
|
+
# You are able to move this logic out into a class if it is too complex for
|
|
1034
|
+
# routes. This class must have a `matches?` method defined on it which either
|
|
1035
|
+
# returns `true` if the user should be given access to that route, or `false` if
|
|
1036
|
+
# the user should not.
|
|
1037
|
+
#
|
|
1038
|
+
# class Iphone
|
|
1039
|
+
# def self.matches?(request)
|
|
1040
|
+
# /iPhone/.match?(request.env["HTTP_USER_AGENT"])
|
|
1041
|
+
# end
|
|
1042
|
+
# end
|
|
1043
|
+
#
|
|
1044
|
+
# An expected place for this code would be `lib/constraints`.
|
|
1045
|
+
#
|
|
1046
|
+
# This class is then used like this:
|
|
1047
|
+
#
|
|
1048
|
+
# constraints(Iphone) do
|
|
1049
|
+
# resources :iphones
|
|
1050
|
+
# end
|
|
1051
|
+
def constraints(constraints = {}, &block)
|
|
1052
|
+
scope(constraints: constraints, &block)
|
|
1053
|
+
end
|
|
1054
|
+
|
|
1055
|
+
# Allows you to set default parameters for a route, such as this:
|
|
1056
|
+
# defaults id: 'home' do
|
|
1057
|
+
# match 'scoped_pages/(:id)', to: 'pages#show'
|
|
1058
|
+
# end
|
|
1059
|
+
#
|
|
1060
|
+
# Using this, the `:id` parameter here will default to 'home'.
|
|
1061
|
+
def defaults(defaults = {})
|
|
1062
|
+
@scope = @scope.new(defaults: merge_defaults_scope(@scope[:defaults], defaults))
|
|
1063
|
+
yield
|
|
1064
|
+
ensure
|
|
1065
|
+
@scope = @scope.parent
|
|
1066
|
+
end
|
|
1067
|
+
|
|
1068
|
+
private
|
|
1069
|
+
def merge_path_scope(parent, child)
|
|
1070
|
+
Mapper.normalize_path("#{parent}/#{child}")
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
def merge_shallow_path_scope(parent, child)
|
|
1074
|
+
Mapper.normalize_path("#{parent}/#{child}")
|
|
1075
|
+
end
|
|
1076
|
+
|
|
1077
|
+
def merge_as_scope(parent, child)
|
|
1078
|
+
parent ? "#{parent}_#{child}" : child
|
|
1079
|
+
end
|
|
1080
|
+
|
|
1081
|
+
def merge_shallow_prefix_scope(parent, child)
|
|
1082
|
+
parent ? "#{parent}_#{child}" : child
|
|
1083
|
+
end
|
|
1084
|
+
|
|
1085
|
+
def merge_module_scope(parent, child)
|
|
1086
|
+
parent ? "#{parent}/#{child}" : child
|
|
1087
|
+
end
|
|
1088
|
+
|
|
1089
|
+
def merge_controller_scope(parent, child)
|
|
1090
|
+
child
|
|
1091
|
+
end
|
|
1092
|
+
|
|
1093
|
+
def merge_action_scope(parent, child)
|
|
1094
|
+
child
|
|
1095
|
+
end
|
|
1096
|
+
|
|
1097
|
+
def merge_via_scope(parent, child)
|
|
1098
|
+
child
|
|
1099
|
+
end
|
|
1100
|
+
|
|
1101
|
+
def merge_format_scope(parent, child)
|
|
1102
|
+
child
|
|
1103
|
+
end
|
|
1104
|
+
|
|
1105
|
+
def merge_path_names_scope(parent, child)
|
|
1106
|
+
merge_options_scope(parent, child)
|
|
1107
|
+
end
|
|
1108
|
+
|
|
1109
|
+
def merge_constraints_scope(parent, child)
|
|
1110
|
+
merge_options_scope(parent, child)
|
|
1111
|
+
end
|
|
1112
|
+
|
|
1113
|
+
def merge_defaults_scope(parent, child)
|
|
1114
|
+
merge_options_scope(parent, child)
|
|
1115
|
+
end
|
|
1116
|
+
|
|
1117
|
+
def merge_blocks_scope(parent, child)
|
|
1118
|
+
merged = parent ? parent.dup : []
|
|
1119
|
+
merged << child if child
|
|
1120
|
+
merged
|
|
1121
|
+
end
|
|
1122
|
+
|
|
1123
|
+
def merge_options_scope(parent, child)
|
|
1124
|
+
(parent || {}).merge(child)
|
|
1125
|
+
end
|
|
1126
|
+
|
|
1127
|
+
def merge_shallow_scope(parent, child)
|
|
1128
|
+
child ? true : false
|
|
1129
|
+
end
|
|
1130
|
+
|
|
1131
|
+
def merge_to_scope(parent, child)
|
|
1132
|
+
child
|
|
1133
|
+
end
|
|
1134
|
+
end
|
|
1135
|
+
|
|
1136
|
+
# Resource routing allows you to quickly declare all of the common routes for a
|
|
1137
|
+
# given resourceful controller. Instead of declaring separate routes for your
|
|
1138
|
+
# `index`, `show`, `new`, `edit`, `create`, `update`, and `destroy` actions, a
|
|
1139
|
+
# resourceful route declares them in a single line of code:
|
|
1140
|
+
#
|
|
1141
|
+
# resources :photos
|
|
1142
|
+
#
|
|
1143
|
+
# Sometimes, you have a resource that clients always look up without referencing
|
|
1144
|
+
# an ID. A common example, /profile always shows the profile of the currently
|
|
1145
|
+
# logged in user. In this case, you can use a singular resource to map /profile
|
|
1146
|
+
# (rather than /profile/:id) to the show action.
|
|
1147
|
+
#
|
|
1148
|
+
# resource :profile
|
|
1149
|
+
#
|
|
1150
|
+
# It's common to have resources that are logically children of other resources:
|
|
1151
|
+
#
|
|
1152
|
+
# resources :magazines do
|
|
1153
|
+
# resources :ads
|
|
1154
|
+
# end
|
|
1155
|
+
#
|
|
1156
|
+
# You may wish to organize groups of controllers under a namespace. Most
|
|
1157
|
+
# commonly, you might group a number of administrative controllers under an
|
|
1158
|
+
# `admin` namespace. You would place these controllers under the
|
|
1159
|
+
# `app/controllers/admin` directory, and you can group them together in your
|
|
1160
|
+
# router:
|
|
1161
|
+
#
|
|
1162
|
+
# namespace "admin" do
|
|
1163
|
+
# resources :posts, :comments
|
|
1164
|
+
# end
|
|
1165
|
+
#
|
|
1166
|
+
# By default the `:id` parameter doesn't accept dots. If you need to use dots as
|
|
1167
|
+
# part of the `:id` parameter add a constraint which overrides this restriction,
|
|
1168
|
+
# e.g:
|
|
1169
|
+
#
|
|
1170
|
+
# resources :articles, id: /[^\/]+/
|
|
1171
|
+
#
|
|
1172
|
+
# This allows any character other than a slash as part of your `:id`.
|
|
1173
|
+
module Resources
|
|
1174
|
+
# CANONICAL_ACTIONS holds all actions that does not need a prefix or a path
|
|
1175
|
+
# appended since they fit properly in their scope level.
|
|
1176
|
+
VALID_ON_OPTIONS = [:new, :collection, :member]
|
|
1177
|
+
RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns]
|
|
1178
|
+
CANONICAL_ACTIONS = %w(index create new show update destroy)
|
|
1179
|
+
|
|
1180
|
+
class Resource # :nodoc:
|
|
1181
|
+
attr_reader :controller, :path, :param
|
|
1182
|
+
|
|
1183
|
+
def initialize(entities, api_only, shallow, options = {})
|
|
1184
|
+
if options[:param].to_s.include?(":")
|
|
1185
|
+
raise ArgumentError, ":param option can't contain colons"
|
|
1186
|
+
end
|
|
1187
|
+
|
|
1188
|
+
@name = entities.to_s
|
|
1189
|
+
@path = (options[:path] || @name).to_s
|
|
1190
|
+
@controller = (options[:controller] || @name).to_s
|
|
1191
|
+
@as = options[:as]
|
|
1192
|
+
@param = (options[:param] || :id).to_sym
|
|
1193
|
+
@options = options
|
|
1194
|
+
@shallow = shallow
|
|
1195
|
+
@api_only = api_only
|
|
1196
|
+
@only = options.delete :only
|
|
1197
|
+
@except = options.delete :except
|
|
1198
|
+
end
|
|
1199
|
+
|
|
1200
|
+
def default_actions
|
|
1201
|
+
if @api_only
|
|
1202
|
+
[:index, :create, :show, :update, :destroy]
|
|
1203
|
+
else
|
|
1204
|
+
[:index, :create, :new, :show, :update, :destroy, :edit]
|
|
1205
|
+
end
|
|
1206
|
+
end
|
|
1207
|
+
|
|
1208
|
+
def actions
|
|
1209
|
+
if @except
|
|
1210
|
+
available_actions - Array(@except).map(&:to_sym)
|
|
1211
|
+
else
|
|
1212
|
+
available_actions
|
|
1213
|
+
end
|
|
1214
|
+
end
|
|
1215
|
+
|
|
1216
|
+
def available_actions
|
|
1217
|
+
if @only
|
|
1218
|
+
Array(@only).map(&:to_sym)
|
|
1219
|
+
else
|
|
1220
|
+
default_actions
|
|
1221
|
+
end
|
|
1222
|
+
end
|
|
1223
|
+
|
|
1224
|
+
def name
|
|
1225
|
+
@as || @name
|
|
1226
|
+
end
|
|
1227
|
+
|
|
1228
|
+
def plural
|
|
1229
|
+
@plural ||= name.to_s
|
|
1230
|
+
end
|
|
1231
|
+
|
|
1232
|
+
def singular
|
|
1233
|
+
@singular ||= name.to_s.singularize
|
|
1234
|
+
end
|
|
1235
|
+
|
|
1236
|
+
alias :member_name :singular
|
|
1237
|
+
|
|
1238
|
+
# Checks for uncountable plurals, and appends "_index" if the plural and
|
|
1239
|
+
# singular form are the same.
|
|
1240
|
+
def collection_name
|
|
1241
|
+
singular == plural ? "#{plural}_index" : plural
|
|
1242
|
+
end
|
|
1243
|
+
|
|
1244
|
+
def resource_scope
|
|
1245
|
+
controller
|
|
1246
|
+
end
|
|
1247
|
+
|
|
1248
|
+
alias :collection_scope :path
|
|
1249
|
+
|
|
1250
|
+
def member_scope
|
|
1251
|
+
"#{path}/:#{param}"
|
|
1252
|
+
end
|
|
1253
|
+
|
|
1254
|
+
alias :shallow_scope :member_scope
|
|
1255
|
+
|
|
1256
|
+
def new_scope(new_path)
|
|
1257
|
+
"#{path}/#{new_path}"
|
|
1258
|
+
end
|
|
1259
|
+
|
|
1260
|
+
def nested_param
|
|
1261
|
+
:"#{singular}_#{param}"
|
|
1262
|
+
end
|
|
1263
|
+
|
|
1264
|
+
def nested_scope
|
|
1265
|
+
"#{path}/:#{nested_param}"
|
|
1266
|
+
end
|
|
1267
|
+
|
|
1268
|
+
def shallow?
|
|
1269
|
+
@shallow
|
|
1270
|
+
end
|
|
1271
|
+
|
|
1272
|
+
def singleton?; false; end
|
|
1273
|
+
end
|
|
1274
|
+
|
|
1275
|
+
class SingletonResource < Resource # :nodoc:
|
|
1276
|
+
def initialize(entities, api_only, shallow, options)
|
|
1277
|
+
super
|
|
1278
|
+
@as = nil
|
|
1279
|
+
@controller = (options[:controller] || plural).to_s
|
|
1280
|
+
@as = options[:as]
|
|
1281
|
+
end
|
|
1282
|
+
|
|
1283
|
+
def default_actions
|
|
1284
|
+
if @api_only
|
|
1285
|
+
[:show, :create, :update, :destroy]
|
|
1286
|
+
else
|
|
1287
|
+
[:show, :create, :update, :destroy, :new, :edit]
|
|
1288
|
+
end
|
|
1289
|
+
end
|
|
1290
|
+
|
|
1291
|
+
def plural
|
|
1292
|
+
@plural ||= name.to_s.pluralize
|
|
1293
|
+
end
|
|
1294
|
+
|
|
1295
|
+
def singular
|
|
1296
|
+
@singular ||= name.to_s
|
|
1297
|
+
end
|
|
1298
|
+
|
|
1299
|
+
alias :member_name :singular
|
|
1300
|
+
alias :collection_name :singular
|
|
1301
|
+
|
|
1302
|
+
alias :member_scope :path
|
|
1303
|
+
alias :nested_scope :path
|
|
1304
|
+
|
|
1305
|
+
def singleton?; true; end
|
|
1306
|
+
end
|
|
1307
|
+
|
|
1308
|
+
def resources_path_names(options)
|
|
1309
|
+
@scope[:path_names].merge!(options)
|
|
1310
|
+
end
|
|
1311
|
+
|
|
1312
|
+
# Sometimes, you have a resource that clients always look up without referencing
|
|
1313
|
+
# an ID. A common example, /profile always shows the profile of the currently
|
|
1314
|
+
# logged in user. In this case, you can use a singular resource to map /profile
|
|
1315
|
+
# (rather than /profile/:id) to the show action:
|
|
1316
|
+
#
|
|
1317
|
+
# resource :profile
|
|
1318
|
+
#
|
|
1319
|
+
# This creates six different routes in your application, all mapping to the
|
|
1320
|
+
# `Profiles` controller (note that the controller is named after the plural):
|
|
1321
|
+
#
|
|
1322
|
+
# GET /profile/new
|
|
1323
|
+
# GET /profile
|
|
1324
|
+
# GET /profile/edit
|
|
1325
|
+
# PATCH/PUT /profile
|
|
1326
|
+
# DELETE /profile
|
|
1327
|
+
# POST /profile
|
|
1328
|
+
#
|
|
1329
|
+
# If you want instances of a model to work with this resource via record
|
|
1330
|
+
# identification (e.g. in `form_with` or `redirect_to`), you will need to call
|
|
1331
|
+
# [resolve](rdoc-ref:CustomUrls#resolve):
|
|
1332
|
+
#
|
|
1333
|
+
# resource :profile
|
|
1334
|
+
# resolve('Profile') { [:profile] }
|
|
1335
|
+
#
|
|
1336
|
+
# # Enables this to work with singular routes:
|
|
1337
|
+
# form_with(model: @profile) {}
|
|
1338
|
+
#
|
|
1339
|
+
# ### Options
|
|
1340
|
+
# Takes same options as [resources](rdoc-ref:#resources)
|
|
1341
|
+
def resource(*resources, &block)
|
|
1342
|
+
options = resources.extract_options!.dup
|
|
1343
|
+
|
|
1344
|
+
if apply_common_behavior_for(:resource, resources, options, &block)
|
|
1345
|
+
return self
|
|
1346
|
+
end
|
|
1347
|
+
|
|
1348
|
+
with_scope_level(:resource) do
|
|
1349
|
+
options = apply_action_options options
|
|
1350
|
+
resource_scope(SingletonResource.new(resources.pop, api_only?, @scope[:shallow], options)) do
|
|
1351
|
+
yield if block_given?
|
|
1352
|
+
|
|
1353
|
+
concerns(options[:concerns]) if options[:concerns]
|
|
1354
|
+
|
|
1355
|
+
new do
|
|
1356
|
+
get :new
|
|
1357
|
+
end if parent_resource.actions.include?(:new)
|
|
1358
|
+
|
|
1359
|
+
set_member_mappings_for_resource
|
|
1360
|
+
|
|
1361
|
+
collection do
|
|
1362
|
+
post :create
|
|
1363
|
+
end if parent_resource.actions.include?(:create)
|
|
1364
|
+
end
|
|
1365
|
+
end
|
|
1366
|
+
|
|
1367
|
+
self
|
|
1368
|
+
end
|
|
1369
|
+
|
|
1370
|
+
# In Rails, a resourceful route provides a mapping between HTTP verbs and URLs
|
|
1371
|
+
# and controller actions. By convention, each action also maps to particular
|
|
1372
|
+
# CRUD operations in a database. A single entry in the routing file, such as
|
|
1373
|
+
#
|
|
1374
|
+
# resources :photos
|
|
1375
|
+
#
|
|
1376
|
+
# creates seven different routes in your application, all mapping to the
|
|
1377
|
+
# `Photos` controller:
|
|
1378
|
+
#
|
|
1379
|
+
# GET /photos
|
|
1380
|
+
# GET /photos/new
|
|
1381
|
+
# POST /photos
|
|
1382
|
+
# GET /photos/:id
|
|
1383
|
+
# GET /photos/:id/edit
|
|
1384
|
+
# PATCH/PUT /photos/:id
|
|
1385
|
+
# DELETE /photos/:id
|
|
1386
|
+
#
|
|
1387
|
+
# Resources can also be nested infinitely by using this block syntax:
|
|
1388
|
+
#
|
|
1389
|
+
# resources :photos do
|
|
1390
|
+
# resources :comments
|
|
1391
|
+
# end
|
|
1392
|
+
#
|
|
1393
|
+
# This generates the following comments routes:
|
|
1394
|
+
#
|
|
1395
|
+
# GET /photos/:photo_id/comments
|
|
1396
|
+
# GET /photos/:photo_id/comments/new
|
|
1397
|
+
# POST /photos/:photo_id/comments
|
|
1398
|
+
# GET /photos/:photo_id/comments/:id
|
|
1399
|
+
# GET /photos/:photo_id/comments/:id/edit
|
|
1400
|
+
# PATCH/PUT /photos/:photo_id/comments/:id
|
|
1401
|
+
# DELETE /photos/:photo_id/comments/:id
|
|
1402
|
+
#
|
|
1403
|
+
# ### Options
|
|
1404
|
+
# Takes same options as [match](rdoc-ref:Base#match) as well as:
|
|
1405
|
+
#
|
|
1406
|
+
# :path_names
|
|
1407
|
+
# : Allows you to change the segment component of the `edit` and `new`
|
|
1408
|
+
# actions. Actions not specified are not changed.
|
|
1409
|
+
#
|
|
1410
|
+
# resources :posts, path_names: { new: "brand_new" }
|
|
1411
|
+
#
|
|
1412
|
+
# The above example will now change /posts/new to /posts/brand_new.
|
|
1413
|
+
#
|
|
1414
|
+
# :path
|
|
1415
|
+
# : Allows you to change the path prefix for the resource.
|
|
1416
|
+
#
|
|
1417
|
+
# resources :posts, path: 'postings'
|
|
1418
|
+
#
|
|
1419
|
+
# The resource and all segments will now route to /postings instead of
|
|
1420
|
+
# /posts.
|
|
1421
|
+
#
|
|
1422
|
+
# :only
|
|
1423
|
+
# : Only generate routes for the given actions.
|
|
1424
|
+
#
|
|
1425
|
+
# resources :cows, only: :show
|
|
1426
|
+
# resources :cows, only: [:show, :index]
|
|
1427
|
+
#
|
|
1428
|
+
# :except
|
|
1429
|
+
# : Generate all routes except for the given actions.
|
|
1430
|
+
#
|
|
1431
|
+
# resources :cows, except: :show
|
|
1432
|
+
# resources :cows, except: [:show, :index]
|
|
1433
|
+
#
|
|
1434
|
+
# :shallow
|
|
1435
|
+
# : Generates shallow routes for nested resource(s). When placed on a parent
|
|
1436
|
+
# resource, generates shallow routes for all nested resources.
|
|
1437
|
+
#
|
|
1438
|
+
# resources :posts, shallow: true do
|
|
1439
|
+
# resources :comments
|
|
1440
|
+
# end
|
|
1441
|
+
#
|
|
1442
|
+
# Is the same as:
|
|
1443
|
+
#
|
|
1444
|
+
# resources :posts do
|
|
1445
|
+
# resources :comments, except: [:show, :edit, :update, :destroy]
|
|
1446
|
+
# end
|
|
1447
|
+
# resources :comments, only: [:show, :edit, :update, :destroy]
|
|
1448
|
+
#
|
|
1449
|
+
# This allows URLs for resources that otherwise would be deeply nested such
|
|
1450
|
+
# as a comment on a blog post like `/posts/a-long-permalink/comments/1234`
|
|
1451
|
+
# to be shortened to just `/comments/1234`.
|
|
1452
|
+
#
|
|
1453
|
+
# Set `shallow: false` on a child resource to ignore a parent's shallow
|
|
1454
|
+
# parameter.
|
|
1455
|
+
#
|
|
1456
|
+
# :shallow_path
|
|
1457
|
+
# : Prefixes nested shallow routes with the specified path.
|
|
1458
|
+
#
|
|
1459
|
+
# scope shallow_path: "sekret" do
|
|
1460
|
+
# resources :posts do
|
|
1461
|
+
# resources :comments, shallow: true
|
|
1462
|
+
# end
|
|
1463
|
+
# end
|
|
1464
|
+
#
|
|
1465
|
+
# The `comments` resource here will have the following routes generated for
|
|
1466
|
+
# it:
|
|
1467
|
+
#
|
|
1468
|
+
# post_comments GET /posts/:post_id/comments(.:format)
|
|
1469
|
+
# post_comments POST /posts/:post_id/comments(.:format)
|
|
1470
|
+
# new_post_comment GET /posts/:post_id/comments/new(.:format)
|
|
1471
|
+
# edit_comment GET /sekret/comments/:id/edit(.:format)
|
|
1472
|
+
# comment GET /sekret/comments/:id(.:format)
|
|
1473
|
+
# comment PATCH/PUT /sekret/comments/:id(.:format)
|
|
1474
|
+
# comment DELETE /sekret/comments/:id(.:format)
|
|
1475
|
+
#
|
|
1476
|
+
# :shallow_prefix
|
|
1477
|
+
# : Prefixes nested shallow route names with specified prefix.
|
|
1478
|
+
#
|
|
1479
|
+
# scope shallow_prefix: "sekret" do
|
|
1480
|
+
# resources :posts do
|
|
1481
|
+
# resources :comments, shallow: true
|
|
1482
|
+
# end
|
|
1483
|
+
# end
|
|
1484
|
+
#
|
|
1485
|
+
# The `comments` resource here will have the following routes generated for
|
|
1486
|
+
# it:
|
|
1487
|
+
#
|
|
1488
|
+
# post_comments GET /posts/:post_id/comments(.:format)
|
|
1489
|
+
# post_comments POST /posts/:post_id/comments(.:format)
|
|
1490
|
+
# new_post_comment GET /posts/:post_id/comments/new(.:format)
|
|
1491
|
+
# edit_sekret_comment GET /comments/:id/edit(.:format)
|
|
1492
|
+
# sekret_comment GET /comments/:id(.:format)
|
|
1493
|
+
# sekret_comment PATCH/PUT /comments/:id(.:format)
|
|
1494
|
+
# sekret_comment DELETE /comments/:id(.:format)
|
|
1495
|
+
#
|
|
1496
|
+
# :format
|
|
1497
|
+
# : Allows you to specify the default value for optional `format` segment or
|
|
1498
|
+
# disable it by supplying `false`.
|
|
1499
|
+
#
|
|
1500
|
+
# :param
|
|
1501
|
+
# : Allows you to override the default param name of `:id` in the URL.
|
|
1502
|
+
#
|
|
1503
|
+
#
|
|
1504
|
+
# ### Examples
|
|
1505
|
+
#
|
|
1506
|
+
# # routes call +Admin::PostsController+
|
|
1507
|
+
# resources :posts, module: "admin"
|
|
1508
|
+
#
|
|
1509
|
+
# # resource actions are at /admin/posts.
|
|
1510
|
+
# resources :posts, path: "admin/posts"
|
|
1511
|
+
def resources(*resources, &block)
|
|
1512
|
+
options = resources.extract_options!.dup
|
|
1513
|
+
|
|
1514
|
+
if apply_common_behavior_for(:resources, resources, options, &block)
|
|
1515
|
+
return self
|
|
1516
|
+
end
|
|
1517
|
+
|
|
1518
|
+
with_scope_level(:resources) do
|
|
1519
|
+
options = apply_action_options options
|
|
1520
|
+
resource_scope(Resource.new(resources.pop, api_only?, @scope[:shallow], options)) do
|
|
1521
|
+
yield if block_given?
|
|
1522
|
+
|
|
1523
|
+
concerns(options[:concerns]) if options[:concerns]
|
|
1524
|
+
|
|
1525
|
+
collection do
|
|
1526
|
+
get :index if parent_resource.actions.include?(:index)
|
|
1527
|
+
post :create if parent_resource.actions.include?(:create)
|
|
1528
|
+
end
|
|
1529
|
+
|
|
1530
|
+
new do
|
|
1531
|
+
get :new
|
|
1532
|
+
end if parent_resource.actions.include?(:new)
|
|
1533
|
+
|
|
1534
|
+
set_member_mappings_for_resource
|
|
1535
|
+
end
|
|
1536
|
+
end
|
|
1537
|
+
|
|
1538
|
+
self
|
|
1539
|
+
end
|
|
1540
|
+
|
|
1541
|
+
# To add a route to the collection:
|
|
1542
|
+
#
|
|
1543
|
+
# resources :photos do
|
|
1544
|
+
# collection do
|
|
1545
|
+
# get 'search'
|
|
1546
|
+
# end
|
|
1547
|
+
# end
|
|
1548
|
+
#
|
|
1549
|
+
# This will enable Rails to recognize paths such as `/photos/search` with GET,
|
|
1550
|
+
# and route to the search action of `PhotosController`. It will also create the
|
|
1551
|
+
# `search_photos_url` and `search_photos_path` route helpers.
|
|
1552
|
+
def collection(&block)
|
|
1553
|
+
unless resource_scope?
|
|
1554
|
+
raise ArgumentError, "can't use collection outside resource(s) scope"
|
|
1555
|
+
end
|
|
1556
|
+
|
|
1557
|
+
with_scope_level(:collection) do
|
|
1558
|
+
path_scope(parent_resource.collection_scope, &block)
|
|
1559
|
+
end
|
|
1560
|
+
end
|
|
1561
|
+
|
|
1562
|
+
# To add a member route, add a member block into the resource block:
|
|
1563
|
+
#
|
|
1564
|
+
# resources :photos do
|
|
1565
|
+
# member do
|
|
1566
|
+
# get 'preview'
|
|
1567
|
+
# end
|
|
1568
|
+
# end
|
|
1569
|
+
#
|
|
1570
|
+
# This will recognize `/photos/1/preview` with GET, and route to the preview
|
|
1571
|
+
# action of `PhotosController`. It will also create the `preview_photo_url` and
|
|
1572
|
+
# `preview_photo_path` helpers.
|
|
1573
|
+
def member(&block)
|
|
1574
|
+
unless resource_scope?
|
|
1575
|
+
raise ArgumentError, "can't use member outside resource(s) scope"
|
|
1576
|
+
end
|
|
1577
|
+
|
|
1578
|
+
with_scope_level(:member) do
|
|
1579
|
+
if shallow?
|
|
1580
|
+
shallow_scope {
|
|
1581
|
+
path_scope(parent_resource.member_scope, &block)
|
|
1582
|
+
}
|
|
1583
|
+
else
|
|
1584
|
+
path_scope(parent_resource.member_scope, &block)
|
|
1585
|
+
end
|
|
1586
|
+
end
|
|
1587
|
+
end
|
|
1588
|
+
|
|
1589
|
+
def new(&block)
|
|
1590
|
+
unless resource_scope?
|
|
1591
|
+
raise ArgumentError, "can't use new outside resource(s) scope"
|
|
1592
|
+
end
|
|
1593
|
+
|
|
1594
|
+
with_scope_level(:new) do
|
|
1595
|
+
path_scope(parent_resource.new_scope(action_path(:new)), &block)
|
|
1596
|
+
end
|
|
1597
|
+
end
|
|
1598
|
+
|
|
1599
|
+
def nested(&block)
|
|
1600
|
+
unless resource_scope?
|
|
1601
|
+
raise ArgumentError, "can't use nested outside resource(s) scope"
|
|
1602
|
+
end
|
|
1603
|
+
|
|
1604
|
+
with_scope_level(:nested) do
|
|
1605
|
+
if shallow? && shallow_nesting_depth >= 1
|
|
1606
|
+
shallow_scope do
|
|
1607
|
+
path_scope(parent_resource.nested_scope) do
|
|
1608
|
+
scope(nested_options, &block)
|
|
1609
|
+
end
|
|
1610
|
+
end
|
|
1611
|
+
else
|
|
1612
|
+
path_scope(parent_resource.nested_scope) do
|
|
1613
|
+
scope(nested_options, &block)
|
|
1614
|
+
end
|
|
1615
|
+
end
|
|
1616
|
+
end
|
|
1617
|
+
end
|
|
1618
|
+
|
|
1619
|
+
# See ActionDispatch::Routing::Mapper::Scoping#namespace.
|
|
1620
|
+
def namespace(path, options = {})
|
|
1621
|
+
if resource_scope?
|
|
1622
|
+
nested { super }
|
|
1623
|
+
else
|
|
1624
|
+
super
|
|
1625
|
+
end
|
|
1626
|
+
end
|
|
1627
|
+
|
|
1628
|
+
def shallow
|
|
1629
|
+
@scope = @scope.new(shallow: true)
|
|
1630
|
+
yield
|
|
1631
|
+
ensure
|
|
1632
|
+
@scope = @scope.parent
|
|
1633
|
+
end
|
|
1634
|
+
|
|
1635
|
+
def shallow?
|
|
1636
|
+
!parent_resource.singleton? && @scope[:shallow]
|
|
1637
|
+
end
|
|
1638
|
+
|
|
1639
|
+
# Loads another routes file with the given `name` located inside the
|
|
1640
|
+
# `config/routes` directory. In that file, you can use the normal routing DSL,
|
|
1641
|
+
# but *do not* surround it with a `Rails.application.routes.draw` block.
|
|
1642
|
+
#
|
|
1643
|
+
# # config/routes.rb
|
|
1644
|
+
# Rails.application.routes.draw do
|
|
1645
|
+
# draw :admin # Loads `config/routes/admin.rb`
|
|
1646
|
+
# draw "third_party/some_gem" # Loads `config/routes/third_party/some_gem.rb`
|
|
1647
|
+
# end
|
|
1648
|
+
#
|
|
1649
|
+
# # config/routes/admin.rb
|
|
1650
|
+
# namespace :admin do
|
|
1651
|
+
# resources :accounts
|
|
1652
|
+
# end
|
|
1653
|
+
#
|
|
1654
|
+
# # config/routes/third_party/some_gem.rb
|
|
1655
|
+
# mount SomeGem::Engine, at: "/some_gem"
|
|
1656
|
+
#
|
|
1657
|
+
# **CAUTION:** Use this feature with care. Having multiple routes files can
|
|
1658
|
+
# negatively impact discoverability and readability. For most applications —
|
|
1659
|
+
# even those with a few hundred routes — it's easier for developers to have a
|
|
1660
|
+
# single routes file.
|
|
1661
|
+
def draw(name)
|
|
1662
|
+
path = @draw_paths.find do |_path|
|
|
1663
|
+
File.exist? "#{_path}/#{name}.rb"
|
|
1664
|
+
end
|
|
1665
|
+
|
|
1666
|
+
unless path
|
|
1667
|
+
msg = "Your router tried to #draw the external file #{name}.rb,\n" \
|
|
1668
|
+
"but the file was not found in:\n\n"
|
|
1669
|
+
msg += @draw_paths.map { |_path| " * #{_path}" }.join("\n")
|
|
1670
|
+
raise ArgumentError, msg
|
|
1671
|
+
end
|
|
1672
|
+
|
|
1673
|
+
route_path = "#{path}/#{name}.rb"
|
|
1674
|
+
instance_eval(File.read(route_path), route_path.to_s)
|
|
1675
|
+
end
|
|
1676
|
+
|
|
1677
|
+
# Matches a URL pattern to one or more routes. For more information, see
|
|
1678
|
+
# [match](rdoc-ref:Base#match).
|
|
1679
|
+
#
|
|
1680
|
+
# match 'path', to: 'controller#action', via: :post
|
|
1681
|
+
# match 'path', 'otherpath', on: :member, via: :get
|
|
1682
|
+
def match(path, *rest, &block)
|
|
1683
|
+
if rest.empty? && Hash === path
|
|
1684
|
+
options = path
|
|
1685
|
+
path, to = options.find { |name, _value| name.is_a?(String) }
|
|
1686
|
+
|
|
1687
|
+
raise ArgumentError, "Route path not specified" if path.nil?
|
|
1688
|
+
|
|
1689
|
+
case to
|
|
1690
|
+
when Symbol
|
|
1691
|
+
options[:action] = to
|
|
1692
|
+
when String
|
|
1693
|
+
if to.include?("#")
|
|
1694
|
+
options[:to] = to
|
|
1695
|
+
else
|
|
1696
|
+
options[:controller] = to
|
|
1697
|
+
end
|
|
1698
|
+
else
|
|
1699
|
+
options[:to] = to
|
|
1700
|
+
end
|
|
1701
|
+
|
|
1702
|
+
options.delete(path)
|
|
1703
|
+
paths = [path]
|
|
1704
|
+
else
|
|
1705
|
+
options = rest.pop || {}
|
|
1706
|
+
paths = [path] + rest
|
|
1707
|
+
end
|
|
1708
|
+
|
|
1709
|
+
if options.key?(:defaults)
|
|
1710
|
+
defaults(options.delete(:defaults)) { map_match(paths, options, &block) }
|
|
1711
|
+
else
|
|
1712
|
+
map_match(paths, options, &block)
|
|
1713
|
+
end
|
|
1714
|
+
end
|
|
1715
|
+
|
|
1716
|
+
# You can specify what Rails should route "/" to with the root method:
|
|
1717
|
+
#
|
|
1718
|
+
# root to: 'pages#main'
|
|
1719
|
+
#
|
|
1720
|
+
# For options, see `match`, as `root` uses it internally.
|
|
1721
|
+
#
|
|
1722
|
+
# You can also pass a string which will expand
|
|
1723
|
+
#
|
|
1724
|
+
# root 'pages#main'
|
|
1725
|
+
#
|
|
1726
|
+
# You should put the root route at the top of `config/routes.rb`, because this
|
|
1727
|
+
# means it will be matched first. As this is the most popular route of most
|
|
1728
|
+
# Rails applications, this is beneficial.
|
|
1729
|
+
def root(path, options = {})
|
|
1730
|
+
if path.is_a?(String)
|
|
1731
|
+
options[:to] = path
|
|
1732
|
+
elsif path.is_a?(Hash) && options.empty?
|
|
1733
|
+
options = path
|
|
1734
|
+
else
|
|
1735
|
+
raise ArgumentError, "must be called with a path and/or options"
|
|
1736
|
+
end
|
|
1737
|
+
|
|
1738
|
+
if @scope.resources?
|
|
1739
|
+
with_scope_level(:root) do
|
|
1740
|
+
path_scope(parent_resource.path) do
|
|
1741
|
+
match_root_route(options)
|
|
1742
|
+
end
|
|
1743
|
+
end
|
|
1744
|
+
else
|
|
1745
|
+
match_root_route(options)
|
|
1746
|
+
end
|
|
1747
|
+
end
|
|
1748
|
+
|
|
1749
|
+
private
|
|
1750
|
+
def parent_resource
|
|
1751
|
+
@scope[:scope_level_resource]
|
|
1752
|
+
end
|
|
1753
|
+
|
|
1754
|
+
def apply_common_behavior_for(method, resources, options, &block)
|
|
1755
|
+
if resources.length > 1
|
|
1756
|
+
resources.each { |r| public_send(method, r, options, &block) }
|
|
1757
|
+
return true
|
|
1758
|
+
end
|
|
1759
|
+
|
|
1760
|
+
if options[:shallow]
|
|
1761
|
+
options.delete(:shallow)
|
|
1762
|
+
shallow do
|
|
1763
|
+
public_send(method, resources.pop, options, &block)
|
|
1764
|
+
end
|
|
1765
|
+
return true
|
|
1766
|
+
end
|
|
1767
|
+
|
|
1768
|
+
if resource_scope?
|
|
1769
|
+
nested { public_send(method, resources.pop, options, &block) }
|
|
1770
|
+
return true
|
|
1771
|
+
end
|
|
1772
|
+
|
|
1773
|
+
options.keys.each do |k|
|
|
1774
|
+
(options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
|
|
1775
|
+
end
|
|
1776
|
+
|
|
1777
|
+
scope_options = options.slice!(*RESOURCE_OPTIONS)
|
|
1778
|
+
unless scope_options.empty?
|
|
1779
|
+
scope(scope_options) do
|
|
1780
|
+
public_send(method, resources.pop, options, &block)
|
|
1781
|
+
end
|
|
1782
|
+
return true
|
|
1783
|
+
end
|
|
1784
|
+
|
|
1785
|
+
false
|
|
1786
|
+
end
|
|
1787
|
+
|
|
1788
|
+
def apply_action_options(options)
|
|
1789
|
+
return options if action_options? options
|
|
1790
|
+
options.merge scope_action_options
|
|
1791
|
+
end
|
|
1792
|
+
|
|
1793
|
+
def action_options?(options)
|
|
1794
|
+
options[:only] || options[:except]
|
|
1795
|
+
end
|
|
1796
|
+
|
|
1797
|
+
def scope_action_options
|
|
1798
|
+
@scope[:action_options] || {}
|
|
1799
|
+
end
|
|
1800
|
+
|
|
1801
|
+
def resource_scope?
|
|
1802
|
+
@scope.resource_scope?
|
|
1803
|
+
end
|
|
1804
|
+
|
|
1805
|
+
def resource_method_scope?
|
|
1806
|
+
@scope.resource_method_scope?
|
|
1807
|
+
end
|
|
1808
|
+
|
|
1809
|
+
def nested_scope?
|
|
1810
|
+
@scope.nested?
|
|
1811
|
+
end
|
|
1812
|
+
|
|
1813
|
+
def with_scope_level(kind) # :doc:
|
|
1814
|
+
@scope = @scope.new_level(kind)
|
|
1815
|
+
yield
|
|
1816
|
+
ensure
|
|
1817
|
+
@scope = @scope.parent
|
|
1818
|
+
end
|
|
1819
|
+
|
|
1820
|
+
def resource_scope(resource, &block)
|
|
1821
|
+
@scope = @scope.new(scope_level_resource: resource)
|
|
1822
|
+
|
|
1823
|
+
controller(resource.resource_scope, &block)
|
|
1824
|
+
ensure
|
|
1825
|
+
@scope = @scope.parent
|
|
1826
|
+
end
|
|
1827
|
+
|
|
1828
|
+
def nested_options
|
|
1829
|
+
options = { as: parent_resource.member_name }
|
|
1830
|
+
options[:constraints] = {
|
|
1831
|
+
parent_resource.nested_param => param_constraint
|
|
1832
|
+
} if param_constraint?
|
|
1833
|
+
|
|
1834
|
+
options
|
|
1835
|
+
end
|
|
1836
|
+
|
|
1837
|
+
def shallow_nesting_depth
|
|
1838
|
+
@scope.find_all { |node|
|
|
1839
|
+
node.frame[:scope_level_resource]
|
|
1840
|
+
}.count { |node| node.frame[:scope_level_resource].shallow? }
|
|
1841
|
+
end
|
|
1842
|
+
|
|
1843
|
+
def param_constraint?
|
|
1844
|
+
@scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
|
|
1845
|
+
end
|
|
1846
|
+
|
|
1847
|
+
def param_constraint
|
|
1848
|
+
@scope[:constraints][parent_resource.param]
|
|
1849
|
+
end
|
|
1850
|
+
|
|
1851
|
+
def canonical_action?(action)
|
|
1852
|
+
resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
|
|
1853
|
+
end
|
|
1854
|
+
|
|
1855
|
+
def shallow_scope
|
|
1856
|
+
scope = { as: @scope[:shallow_prefix],
|
|
1857
|
+
path: @scope[:shallow_path] }
|
|
1858
|
+
@scope = @scope.new scope
|
|
1859
|
+
|
|
1860
|
+
yield
|
|
1861
|
+
ensure
|
|
1862
|
+
@scope = @scope.parent
|
|
1863
|
+
end
|
|
1864
|
+
|
|
1865
|
+
def path_for_action(action, path)
|
|
1866
|
+
return "#{@scope[:path]}/#{path}" if path
|
|
1867
|
+
|
|
1868
|
+
if canonical_action?(action)
|
|
1869
|
+
@scope[:path].to_s
|
|
1870
|
+
else
|
|
1871
|
+
"#{@scope[:path]}/#{action_path(action)}"
|
|
1872
|
+
end
|
|
1873
|
+
end
|
|
1874
|
+
|
|
1875
|
+
def action_path(name)
|
|
1876
|
+
@scope[:path_names][name.to_sym] || name
|
|
1877
|
+
end
|
|
1878
|
+
|
|
1879
|
+
def prefix_name_for_action(as, action)
|
|
1880
|
+
if as
|
|
1881
|
+
prefix = as
|
|
1882
|
+
elsif !canonical_action?(action)
|
|
1883
|
+
prefix = action
|
|
1884
|
+
end
|
|
1885
|
+
|
|
1886
|
+
if prefix && prefix != "/" && !prefix.empty?
|
|
1887
|
+
Mapper.normalize_name prefix.to_s.tr("-", "_")
|
|
1888
|
+
end
|
|
1889
|
+
end
|
|
1890
|
+
|
|
1891
|
+
def name_for_action(as, action)
|
|
1892
|
+
prefix = prefix_name_for_action(as, action)
|
|
1893
|
+
name_prefix = @scope[:as]
|
|
1894
|
+
|
|
1895
|
+
if parent_resource
|
|
1896
|
+
return nil unless as || action
|
|
1897
|
+
|
|
1898
|
+
collection_name = parent_resource.collection_name
|
|
1899
|
+
member_name = parent_resource.member_name
|
|
1900
|
+
end
|
|
1901
|
+
|
|
1902
|
+
action_name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
|
|
1903
|
+
candidate = action_name.select(&:present?).join("_")
|
|
1904
|
+
|
|
1905
|
+
unless candidate.empty?
|
|
1906
|
+
# If a name was not explicitly given, we check if it is valid and return nil in
|
|
1907
|
+
# case it isn't. Otherwise, we pass the invalid name forward so the underlying
|
|
1908
|
+
# router engine treats it and raises an exception.
|
|
1909
|
+
if as.nil?
|
|
1910
|
+
candidate unless !candidate.match?(/\A[_a-z]/i) || has_named_route?(candidate)
|
|
1911
|
+
else
|
|
1912
|
+
candidate
|
|
1913
|
+
end
|
|
1914
|
+
end
|
|
1915
|
+
end
|
|
1916
|
+
|
|
1917
|
+
def set_member_mappings_for_resource # :doc:
|
|
1918
|
+
member do
|
|
1919
|
+
get :edit if parent_resource.actions.include?(:edit)
|
|
1920
|
+
get :show if parent_resource.actions.include?(:show)
|
|
1921
|
+
if parent_resource.actions.include?(:update)
|
|
1922
|
+
patch :update
|
|
1923
|
+
put :update
|
|
1924
|
+
end
|
|
1925
|
+
delete :destroy if parent_resource.actions.include?(:destroy)
|
|
1926
|
+
end
|
|
1927
|
+
end
|
|
1928
|
+
|
|
1929
|
+
def api_only? # :doc:
|
|
1930
|
+
@set.api_only?
|
|
1931
|
+
end
|
|
1932
|
+
|
|
1933
|
+
def path_scope(path)
|
|
1934
|
+
@scope = @scope.new(path: merge_path_scope(@scope[:path], path))
|
|
1935
|
+
yield
|
|
1936
|
+
ensure
|
|
1937
|
+
@scope = @scope.parent
|
|
1938
|
+
end
|
|
1939
|
+
|
|
1940
|
+
def map_match(paths, options)
|
|
1941
|
+
ActionDispatch.deprecator.warn(<<-MSG.squish) if paths.count > 1
|
|
1942
|
+
Mapping a route with multiple paths is deprecated and
|
|
1943
|
+
will be removed in Rails 8.1. Please use multiple method calls instead.
|
|
1944
|
+
MSG
|
|
1945
|
+
|
|
1946
|
+
if (on = options[:on]) && !VALID_ON_OPTIONS.include?(on)
|
|
1947
|
+
raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
|
|
1948
|
+
end
|
|
1949
|
+
|
|
1950
|
+
if @scope[:to]
|
|
1951
|
+
options[:to] ||= @scope[:to]
|
|
1952
|
+
end
|
|
1953
|
+
|
|
1954
|
+
if @scope[:controller] && @scope[:action]
|
|
1955
|
+
options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
|
|
1956
|
+
end
|
|
1957
|
+
|
|
1958
|
+
controller = options.delete(:controller) || @scope[:controller]
|
|
1959
|
+
option_path = options.delete :path
|
|
1960
|
+
to = options.delete :to
|
|
1961
|
+
via = Mapping.check_via Array(options.delete(:via) {
|
|
1962
|
+
@scope[:via]
|
|
1963
|
+
})
|
|
1964
|
+
formatted = options.delete(:format) { @scope[:format] }
|
|
1965
|
+
anchor = options.delete(:anchor) { true }
|
|
1966
|
+
options_constraints = options.delete(:constraints) || {}
|
|
1967
|
+
|
|
1968
|
+
path_types = paths.group_by(&:class)
|
|
1969
|
+
(path_types[String] || []).each do |_path|
|
|
1970
|
+
route_options = options.dup
|
|
1971
|
+
if _path && option_path
|
|
1972
|
+
raise ArgumentError, "Ambiguous route definition. Both :path and the route path were specified as strings."
|
|
1973
|
+
end
|
|
1974
|
+
to = get_to_from_path(_path, to, route_options[:action])
|
|
1975
|
+
decomposed_match(_path, controller, route_options, _path, to, via, formatted, anchor, options_constraints)
|
|
1976
|
+
end
|
|
1977
|
+
|
|
1978
|
+
(path_types[Symbol] || []).each do |action|
|
|
1979
|
+
route_options = options.dup
|
|
1980
|
+
decomposed_match(action, controller, route_options, option_path, to, via, formatted, anchor, options_constraints)
|
|
1981
|
+
end
|
|
1982
|
+
|
|
1983
|
+
self
|
|
1984
|
+
end
|
|
1985
|
+
|
|
1986
|
+
def get_to_from_path(path, to, action)
|
|
1987
|
+
return to if to || action
|
|
1988
|
+
|
|
1989
|
+
path_without_format = path.sub(/\(\.:format\)$/, "")
|
|
1990
|
+
if using_match_shorthand?(path_without_format)
|
|
1991
|
+
path_without_format.delete_prefix("/").sub(%r{/([^/]*)$}, '#\1').tr("-", "_")
|
|
1992
|
+
else
|
|
1993
|
+
nil
|
|
1994
|
+
end
|
|
1995
|
+
end
|
|
1996
|
+
|
|
1997
|
+
def using_match_shorthand?(path)
|
|
1998
|
+
%r{^/?[-\w]+/[-\w/]+$}.match?(path)
|
|
1999
|
+
end
|
|
2000
|
+
|
|
2001
|
+
def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
|
|
2002
|
+
if on = options.delete(:on)
|
|
2003
|
+
send(on) { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
|
|
2004
|
+
else
|
|
2005
|
+
case @scope.scope_level
|
|
2006
|
+
when :resources
|
|
2007
|
+
nested { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
|
|
2008
|
+
when :resource
|
|
2009
|
+
member { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
|
|
2010
|
+
else
|
|
2011
|
+
add_route(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
|
|
2012
|
+
end
|
|
2013
|
+
end
|
|
2014
|
+
end
|
|
2015
|
+
|
|
2016
|
+
def add_route(action, controller, options, _path, to, via, formatted, anchor, options_constraints)
|
|
2017
|
+
path = path_for_action(action, _path)
|
|
2018
|
+
raise ArgumentError, "path is required" if path.blank?
|
|
2019
|
+
|
|
2020
|
+
action = action.to_s
|
|
2021
|
+
|
|
2022
|
+
default_action = options.delete(:action) || @scope[:action]
|
|
2023
|
+
|
|
2024
|
+
if /^[\w\-\/]+$/.match?(action)
|
|
2025
|
+
default_action ||= action.tr("-", "_") unless action.include?("/")
|
|
2026
|
+
else
|
|
2027
|
+
action = nil
|
|
2028
|
+
end
|
|
2029
|
+
|
|
2030
|
+
as = if !options.fetch(:as, true) # if it's set to nil or false
|
|
2031
|
+
options.delete(:as)
|
|
2032
|
+
else
|
|
2033
|
+
name_for_action(options.delete(:as), action)
|
|
2034
|
+
end
|
|
2035
|
+
|
|
2036
|
+
path = Mapping.normalize_path URI::RFC2396_PARSER.escape(path), formatted
|
|
2037
|
+
ast = Journey::Parser.parse path
|
|
2038
|
+
|
|
2039
|
+
mapping = Mapping.build(@scope, @set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
|
|
2040
|
+
@set.add_route(mapping, as)
|
|
2041
|
+
end
|
|
2042
|
+
|
|
2043
|
+
def match_root_route(options)
|
|
2044
|
+
args = ["/", { as: :root, via: :get }.merge(options)]
|
|
2045
|
+
match(*args)
|
|
2046
|
+
end
|
|
2047
|
+
end
|
|
2048
|
+
|
|
2049
|
+
# Routing Concerns allow you to declare common routes that can be reused inside
|
|
2050
|
+
# others resources and routes.
|
|
2051
|
+
#
|
|
2052
|
+
# concern :commentable do
|
|
2053
|
+
# resources :comments
|
|
2054
|
+
# end
|
|
2055
|
+
#
|
|
2056
|
+
# concern :image_attachable do
|
|
2057
|
+
# resources :images, only: :index
|
|
2058
|
+
# end
|
|
2059
|
+
#
|
|
2060
|
+
# These concerns are used in Resources routing:
|
|
2061
|
+
#
|
|
2062
|
+
# resources :messages, concerns: [:commentable, :image_attachable]
|
|
2063
|
+
#
|
|
2064
|
+
# or in a scope or namespace:
|
|
2065
|
+
#
|
|
2066
|
+
# namespace :posts do
|
|
2067
|
+
# concerns :commentable
|
|
2068
|
+
# end
|
|
2069
|
+
module Concerns
|
|
2070
|
+
# Define a routing concern using a name.
|
|
2071
|
+
#
|
|
2072
|
+
# Concerns may be defined inline, using a block, or handled by another object,
|
|
2073
|
+
# by passing that object as the second parameter.
|
|
2074
|
+
#
|
|
2075
|
+
# The concern object, if supplied, should respond to `call`, which will receive
|
|
2076
|
+
# two parameters:
|
|
2077
|
+
#
|
|
2078
|
+
# * The current mapper
|
|
2079
|
+
# * A hash of options which the concern object may use
|
|
2080
|
+
#
|
|
2081
|
+
# Options may also be used by concerns defined in a block by accepting a block
|
|
2082
|
+
# parameter. So, using a block, you might do something as simple as limit the
|
|
2083
|
+
# actions available on certain resources, passing standard resource options
|
|
2084
|
+
# through the concern:
|
|
2085
|
+
#
|
|
2086
|
+
# concern :commentable do |options|
|
|
2087
|
+
# resources :comments, options
|
|
2088
|
+
# end
|
|
2089
|
+
#
|
|
2090
|
+
# resources :posts, concerns: :commentable
|
|
2091
|
+
# resources :archived_posts do
|
|
2092
|
+
# # Don't allow comments on archived posts
|
|
2093
|
+
# concerns :commentable, only: [:index, :show]
|
|
2094
|
+
# end
|
|
2095
|
+
#
|
|
2096
|
+
# Or, using a callable object, you might implement something more specific to
|
|
2097
|
+
# your application, which would be out of place in your routes file.
|
|
2098
|
+
#
|
|
2099
|
+
# # purchasable.rb
|
|
2100
|
+
# class Purchasable
|
|
2101
|
+
# def initialize(defaults = {})
|
|
2102
|
+
# @defaults = defaults
|
|
2103
|
+
# end
|
|
2104
|
+
#
|
|
2105
|
+
# def call(mapper, options = {})
|
|
2106
|
+
# options = @defaults.merge(options)
|
|
2107
|
+
# mapper.resources :purchases
|
|
2108
|
+
# mapper.resources :receipts
|
|
2109
|
+
# mapper.resources :returns if options[:returnable]
|
|
2110
|
+
# end
|
|
2111
|
+
# end
|
|
2112
|
+
#
|
|
2113
|
+
# # routes.rb
|
|
2114
|
+
# concern :purchasable, Purchasable.new(returnable: true)
|
|
2115
|
+
#
|
|
2116
|
+
# resources :toys, concerns: :purchasable
|
|
2117
|
+
# resources :electronics, concerns: :purchasable
|
|
2118
|
+
# resources :pets do
|
|
2119
|
+
# concerns :purchasable, returnable: false
|
|
2120
|
+
# end
|
|
2121
|
+
#
|
|
2122
|
+
# Any routing helpers can be used inside a concern. If using a callable, they're
|
|
2123
|
+
# accessible from the Mapper that's passed to `call`.
|
|
2124
|
+
def concern(name, callable = nil, &block)
|
|
2125
|
+
callable ||= lambda { |mapper, options| mapper.instance_exec(options, &block) }
|
|
2126
|
+
@concerns[name] = callable
|
|
2127
|
+
end
|
|
2128
|
+
|
|
2129
|
+
# Use the named concerns
|
|
2130
|
+
#
|
|
2131
|
+
# resources :posts do
|
|
2132
|
+
# concerns :commentable
|
|
2133
|
+
# end
|
|
2134
|
+
#
|
|
2135
|
+
# Concerns also work in any routes helper that you want to use:
|
|
2136
|
+
#
|
|
2137
|
+
# namespace :posts do
|
|
2138
|
+
# concerns :commentable
|
|
2139
|
+
# end
|
|
2140
|
+
def concerns(*args)
|
|
2141
|
+
options = args.extract_options!
|
|
2142
|
+
args.flatten.each do |name|
|
|
2143
|
+
if concern = @concerns[name]
|
|
2144
|
+
concern.call(self, options)
|
|
2145
|
+
else
|
|
2146
|
+
raise ArgumentError, "No concern named #{name} was found!"
|
|
2147
|
+
end
|
|
2148
|
+
end
|
|
2149
|
+
end
|
|
2150
|
+
end
|
|
2151
|
+
|
|
2152
|
+
module CustomUrls
|
|
2153
|
+
# Define custom URL helpers that will be added to the application's routes. This
|
|
2154
|
+
# allows you to override and/or replace the default behavior of routing helpers,
|
|
2155
|
+
# e.g:
|
|
2156
|
+
#
|
|
2157
|
+
# direct :homepage do
|
|
2158
|
+
# "https://rubyonrails.org"
|
|
2159
|
+
# end
|
|
2160
|
+
#
|
|
2161
|
+
# direct :commentable do |model|
|
|
2162
|
+
# [ model, anchor: model.dom_id ]
|
|
2163
|
+
# end
|
|
2164
|
+
#
|
|
2165
|
+
# direct :main do
|
|
2166
|
+
# { controller: "pages", action: "index", subdomain: "www" }
|
|
2167
|
+
# end
|
|
2168
|
+
#
|
|
2169
|
+
# The return value from the block passed to `direct` must be a valid set of
|
|
2170
|
+
# arguments for `url_for` which will actually build the URL string. This can be
|
|
2171
|
+
# one of the following:
|
|
2172
|
+
#
|
|
2173
|
+
# * A string, which is treated as a generated URL
|
|
2174
|
+
# * A hash, e.g. `{ controller: "pages", action: "index" }`
|
|
2175
|
+
# * An array, which is passed to `polymorphic_url`
|
|
2176
|
+
# * An Active Model instance
|
|
2177
|
+
# * An Active Model class
|
|
2178
|
+
#
|
|
2179
|
+
#
|
|
2180
|
+
# NOTE: Other URL helpers can be called in the block but be careful not to
|
|
2181
|
+
# invoke your custom URL helper again otherwise it will result in a stack
|
|
2182
|
+
# overflow error.
|
|
2183
|
+
#
|
|
2184
|
+
# You can also specify default options that will be passed through to your URL
|
|
2185
|
+
# helper definition, e.g:
|
|
2186
|
+
#
|
|
2187
|
+
# direct :browse, page: 1, size: 10 do |options|
|
|
2188
|
+
# [ :products, options.merge(params.permit(:page, :size).to_h.symbolize_keys) ]
|
|
2189
|
+
# end
|
|
2190
|
+
#
|
|
2191
|
+
# In this instance the `params` object comes from the context in which the block
|
|
2192
|
+
# is executed, e.g. generating a URL inside a controller action or a view. If
|
|
2193
|
+
# the block is executed where there isn't a `params` object such as this:
|
|
2194
|
+
#
|
|
2195
|
+
# Rails.application.routes.url_helpers.browse_path
|
|
2196
|
+
#
|
|
2197
|
+
# then it will raise a `NameError`. Because of this you need to be aware of the
|
|
2198
|
+
# context in which you will use your custom URL helper when defining it.
|
|
2199
|
+
#
|
|
2200
|
+
# NOTE: The `direct` method can't be used inside of a scope block such as
|
|
2201
|
+
# `namespace` or `scope` and will raise an error if it detects that it is.
|
|
2202
|
+
def direct(name, options = {}, &block)
|
|
2203
|
+
unless @scope.root?
|
|
2204
|
+
raise RuntimeError, "The direct method can't be used inside a routes scope block"
|
|
2205
|
+
end
|
|
2206
|
+
|
|
2207
|
+
@set.add_url_helper(name, options, &block)
|
|
2208
|
+
end
|
|
2209
|
+
|
|
2210
|
+
# Define custom polymorphic mappings of models to URLs. This alters the behavior
|
|
2211
|
+
# of `polymorphic_url` and consequently the behavior of `link_to` and `form_for`
|
|
2212
|
+
# when passed a model instance, e.g:
|
|
2213
|
+
#
|
|
2214
|
+
# resource :basket
|
|
2215
|
+
#
|
|
2216
|
+
# resolve "Basket" do
|
|
2217
|
+
# [:basket]
|
|
2218
|
+
# end
|
|
2219
|
+
#
|
|
2220
|
+
# This will now generate "/basket" when a `Basket` instance is passed to
|
|
2221
|
+
# `link_to` or `form_for` instead of the standard "/baskets/:id".
|
|
2222
|
+
#
|
|
2223
|
+
# NOTE: This custom behavior only applies to simple polymorphic URLs where a
|
|
2224
|
+
# single model instance is passed and not more complicated forms, e.g:
|
|
2225
|
+
#
|
|
2226
|
+
# # config/routes.rb
|
|
2227
|
+
# resource :profile
|
|
2228
|
+
# namespace :admin do
|
|
2229
|
+
# resources :users
|
|
2230
|
+
# end
|
|
2231
|
+
#
|
|
2232
|
+
# resolve("User") { [:profile] }
|
|
2233
|
+
#
|
|
2234
|
+
# # app/views/application/_menu.html.erb
|
|
2235
|
+
# link_to "Profile", @current_user
|
|
2236
|
+
# link_to "Profile", [:admin, @current_user]
|
|
2237
|
+
#
|
|
2238
|
+
# The first `link_to` will generate "/profile" but the second will generate the
|
|
2239
|
+
# standard polymorphic URL of "/admin/users/1".
|
|
2240
|
+
#
|
|
2241
|
+
# You can pass options to a polymorphic mapping - the arity for the block needs
|
|
2242
|
+
# to be two as the instance is passed as the first argument, e.g:
|
|
2243
|
+
#
|
|
2244
|
+
# resolve "Basket", anchor: "items" do |basket, options|
|
|
2245
|
+
# [:basket, options]
|
|
2246
|
+
# end
|
|
2247
|
+
#
|
|
2248
|
+
# This generates the URL "/basket#items" because when the last item in an array
|
|
2249
|
+
# passed to `polymorphic_url` is a hash then it's treated as options to the URL
|
|
2250
|
+
# helper that gets called.
|
|
2251
|
+
#
|
|
2252
|
+
# NOTE: The `resolve` method can't be used inside of a scope block such as
|
|
2253
|
+
# `namespace` or `scope` and will raise an error if it detects that it is.
|
|
2254
|
+
def resolve(*args, &block)
|
|
2255
|
+
unless @scope.root?
|
|
2256
|
+
raise RuntimeError, "The resolve method can't be used inside a routes scope block"
|
|
2257
|
+
end
|
|
2258
|
+
|
|
2259
|
+
options = args.extract_options!
|
|
2260
|
+
args = args.flatten(1)
|
|
2261
|
+
|
|
2262
|
+
args.each do |klass|
|
|
2263
|
+
@set.add_polymorphic_mapping(klass, options, &block)
|
|
2264
|
+
end
|
|
2265
|
+
end
|
|
2266
|
+
end
|
|
2267
|
+
|
|
2268
|
+
class Scope # :nodoc:
|
|
2269
|
+
OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
|
|
2270
|
+
:controller, :action, :path_names, :constraints,
|
|
2271
|
+
:shallow, :blocks, :defaults, :via, :format, :options, :to]
|
|
2272
|
+
|
|
2273
|
+
RESOURCE_SCOPES = [:resource, :resources]
|
|
2274
|
+
RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
|
|
2275
|
+
|
|
2276
|
+
attr_reader :parent, :scope_level
|
|
2277
|
+
|
|
2278
|
+
def initialize(hash, parent = ROOT, scope_level = nil)
|
|
2279
|
+
@parent = parent
|
|
2280
|
+
@hash = parent ? parent.frame.merge(hash) : hash
|
|
2281
|
+
@scope_level = scope_level
|
|
2282
|
+
end
|
|
2283
|
+
|
|
2284
|
+
def nested?
|
|
2285
|
+
scope_level == :nested
|
|
2286
|
+
end
|
|
2287
|
+
|
|
2288
|
+
def null?
|
|
2289
|
+
@hash.nil? && @parent.nil?
|
|
2290
|
+
end
|
|
2291
|
+
|
|
2292
|
+
def root?
|
|
2293
|
+
@parent == ROOT
|
|
2294
|
+
end
|
|
2295
|
+
|
|
2296
|
+
def resources?
|
|
2297
|
+
scope_level == :resources
|
|
2298
|
+
end
|
|
2299
|
+
|
|
2300
|
+
def resource_method_scope?
|
|
2301
|
+
RESOURCE_METHOD_SCOPES.include? scope_level
|
|
2302
|
+
end
|
|
2303
|
+
|
|
2304
|
+
def action_name(name_prefix, prefix, collection_name, member_name)
|
|
2305
|
+
case scope_level
|
|
2306
|
+
when :nested
|
|
2307
|
+
[name_prefix, prefix]
|
|
2308
|
+
when :collection
|
|
2309
|
+
[prefix, name_prefix, collection_name]
|
|
2310
|
+
when :new
|
|
2311
|
+
[prefix, :new, name_prefix, member_name]
|
|
2312
|
+
when :member
|
|
2313
|
+
[prefix, name_prefix, member_name]
|
|
2314
|
+
when :root
|
|
2315
|
+
[name_prefix, collection_name, prefix]
|
|
2316
|
+
else
|
|
2317
|
+
[name_prefix, member_name, prefix]
|
|
2318
|
+
end
|
|
2319
|
+
end
|
|
2320
|
+
|
|
2321
|
+
def resource_scope?
|
|
2322
|
+
RESOURCE_SCOPES.include? scope_level
|
|
2323
|
+
end
|
|
2324
|
+
|
|
2325
|
+
def options
|
|
2326
|
+
OPTIONS
|
|
2327
|
+
end
|
|
2328
|
+
|
|
2329
|
+
def new(hash)
|
|
2330
|
+
self.class.new hash, self, scope_level
|
|
2331
|
+
end
|
|
2332
|
+
|
|
2333
|
+
def new_level(level)
|
|
2334
|
+
self.class.new(frame, self, level)
|
|
2335
|
+
end
|
|
2336
|
+
|
|
2337
|
+
def [](key)
|
|
2338
|
+
frame[key]
|
|
2339
|
+
end
|
|
2340
|
+
|
|
2341
|
+
def frame; @hash; end
|
|
2342
|
+
|
|
2343
|
+
include Enumerable
|
|
2344
|
+
|
|
2345
|
+
def each
|
|
2346
|
+
node = self
|
|
2347
|
+
until node.equal? ROOT
|
|
2348
|
+
yield node
|
|
2349
|
+
node = node.parent
|
|
2350
|
+
end
|
|
2351
|
+
end
|
|
2352
|
+
|
|
2353
|
+
ROOT = Scope.new({}, nil)
|
|
2354
|
+
end
|
|
2355
|
+
|
|
2356
|
+
def initialize(set) # :nodoc:
|
|
2357
|
+
@set = set
|
|
2358
|
+
@draw_paths = set.draw_paths
|
|
2359
|
+
@scope = Scope.new(path_names: @set.resources_path_names)
|
|
2360
|
+
@concerns = {}
|
|
2361
|
+
end
|
|
2362
|
+
|
|
2363
|
+
include Base
|
|
2364
|
+
include HttpHelpers
|
|
2365
|
+
include Redirection
|
|
2366
|
+
include Scoping
|
|
2367
|
+
include Concerns
|
|
2368
|
+
include Resources
|
|
2369
|
+
include CustomUrls
|
|
2370
|
+
end
|
|
2371
|
+
end
|
|
2372
|
+
end
|