fake_rails3_routes 1.0.0
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.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +51 -0
- data/Rakefile +1 -0
- data/fake_rails3_routes.gemspec +25 -0
- data/lib/fake_rails3_routes.rb +48 -0
- data/lib/fake_rails3_routes/mapper.rb +1420 -0
- data/lib/fake_rails3_routes/version.rb +3 -0
- metadata +109 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 The Rails Team and Instructure, Inc.
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# FakeRails3Routes
|
2
|
+
|
3
|
+
This gem adapts the Rails 3 routing code to generate Rails 2.3 routes on
|
4
|
+
the back-end, so that you can upgrade your Rails 2 routes to Rails 3
|
5
|
+
format before your app is completely on Rails 3.
|
6
|
+
|
7
|
+
Why? If you have a Rails 2 application of significant size, and a whole
|
8
|
+
group of programmers actively developing it, then "stopping the world"
|
9
|
+
to upgrade to Rails 3 isn't really an option. Doing it on a dedicated
|
10
|
+
branch isn't really any better, you run into the same issues with
|
11
|
+
diverging codebases. This gem is one component of the infrastructure
|
12
|
+
necessary to upgrade your app "live" one piece at a time.
|
13
|
+
|
14
|
+
## Not supported
|
15
|
+
|
16
|
+
This has only been tested with fairly basic routes. Don't try anything
|
17
|
+
too fancy, its behavior will diverge from Rails 3 routing behavior in
|
18
|
+
some more advanced cases. Some known unsupported functionality:
|
19
|
+
|
20
|
+
* Mounting Rack apps at a path with #mount
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
Add this line to your application's Gemfile:
|
25
|
+
|
26
|
+
gem 'fake_rails3_routes'
|
27
|
+
|
28
|
+
If your Rails 2.3 application doesn't yet use a Gemfile, do that upgrade
|
29
|
+
first.
|
30
|
+
|
31
|
+
Then use the rails_upgrade gem (https://github.com/rails/rails_upgrade)
|
32
|
+
to upgrade your routes file to Rails 3 format. Replace the first line
|
33
|
+
with:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
FakeRails3Routes.draw do
|
37
|
+
```
|
38
|
+
|
39
|
+
## Copyright
|
40
|
+
|
41
|
+
The vast majority of this gem is extracted directly from Rails 3,
|
42
|
+
licensed under the MIT license. The modifications are released under
|
43
|
+
this same license.
|
44
|
+
|
45
|
+
## Contributing
|
46
|
+
|
47
|
+
1. Fork it
|
48
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
49
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
50
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
51
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'fake_rails3_routes/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "fake_rails3_routes"
|
8
|
+
spec.version = FakeRails3Routes::VERSION
|
9
|
+
spec.authors = ["Brian Palmer"]
|
10
|
+
spec.email = ["brianp@instructure.com"]
|
11
|
+
spec.description = %q{Write Rails 3 style routes in Rails 2 apps}
|
12
|
+
spec.summary = %q{Write Rails 3 style routes in Rails 2 apps}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "journey", "1.0.4"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
require "fake_rails3_routes/version"
|
4
|
+
|
5
|
+
module FakeRails3Routes
|
6
|
+
def self.draw(&block)
|
7
|
+
if Rails.version >= "3.0"
|
8
|
+
# we're on rails 3, no need to emulate
|
9
|
+
Rails.application.class.routes.draw(&block)
|
10
|
+
else
|
11
|
+
@route_set ||= FakeRails3Routes::RouteSet.new
|
12
|
+
@route_set.draw(block)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class RouteSet
|
17
|
+
def draw(block)
|
18
|
+
require 'fake_rails3_routes/mapper'
|
19
|
+
require 'journey'
|
20
|
+
ActionController::Routing::Routes.draw do |map|
|
21
|
+
@map = map
|
22
|
+
@named_routes = Set.new
|
23
|
+
mapper = FakeRails3Routes::Mapper.new(self)
|
24
|
+
mapper.instance_exec(&block)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_route(conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true)
|
29
|
+
defaults = defaults.merge(requirements)
|
30
|
+
path = conditions.delete(:path_info)
|
31
|
+
if defaults[:format]
|
32
|
+
path.sub!("(.:format)", '')
|
33
|
+
end
|
34
|
+
if name == 'root'
|
35
|
+
@map.send(name, defaults)
|
36
|
+
elsif name
|
37
|
+
@map.send(name, path, defaults)
|
38
|
+
@named_routes << name
|
39
|
+
else
|
40
|
+
@map.connect(path, defaults)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def named_route?(name)
|
45
|
+
!!(name && @map.instance_variable_get(:@set).named_routes.get(name))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,1420 @@
|
|
1
|
+
module FakeRails3Routes
|
2
|
+
class Mapper
|
3
|
+
SEPARATORS = %w( / . ? ) #:nodoc:
|
4
|
+
HTTP_METHODS = [:get, :head, :post, :put, :delete, :options] #:nodoc:
|
5
|
+
|
6
|
+
def initialize(set)
|
7
|
+
@set = set
|
8
|
+
@scope = { :path_names => { :new => 'new', :edit => 'edit' } }
|
9
|
+
end
|
10
|
+
|
11
|
+
# Invokes Rack::Mount::Utils.normalize path and ensure that
|
12
|
+
# (:locale) becomes (/:locale) instead of /(:locale). Except
|
13
|
+
# for root cases, where the latter is the correct one.
|
14
|
+
def self.normalize_path(path)
|
15
|
+
path = Journey::Router::Utils.normalize_path(path)
|
16
|
+
path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$}
|
17
|
+
path
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.normalize_name(name)
|
21
|
+
normalize_path(name)[1..-1].gsub("/", "_")
|
22
|
+
end
|
23
|
+
|
24
|
+
class Mapping #:nodoc:
|
25
|
+
IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix]
|
26
|
+
ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
|
27
|
+
WILDCARD_PATH = %r{\*([^/\)]+)\)?$}
|
28
|
+
|
29
|
+
def initialize(set, scope, path, options)
|
30
|
+
@set, @scope = set, scope
|
31
|
+
@options = (@scope[:options] || {}).merge(options)
|
32
|
+
@path = normalize_path(path)
|
33
|
+
normalize_options!
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_route
|
37
|
+
#defaults.keys.reverse.each do |k|
|
38
|
+
#defaults[k] = defaults.delete(k) unless k == :controller
|
39
|
+
#end
|
40
|
+
[ conditions, requirements, defaults, @options[:as], @options[:anchor] ]
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def normalize_options!
|
46
|
+
path_without_format = @path.sub(/\(\.:format\)$/, '')
|
47
|
+
|
48
|
+
@options.merge!(default_controller_and_action)
|
49
|
+
|
50
|
+
requirements.each do |name, requirement|
|
51
|
+
# segment_keys.include?(k.to_s) || k == :controller
|
52
|
+
next unless Regexp === requirement && !constraints[name]
|
53
|
+
|
54
|
+
if requirement.source =~ ANCHOR_CHARACTERS_REGEX
|
55
|
+
raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
|
56
|
+
end
|
57
|
+
if requirement.multiline?
|
58
|
+
raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# match "account/overview"
|
64
|
+
def using_match_shorthand?(path, options)
|
65
|
+
path && (options[:to] || options[:action]).nil? && path =~ SHORTHAND_REGEX
|
66
|
+
end
|
67
|
+
|
68
|
+
def normalize_path(path)
|
69
|
+
raise ArgumentError, "path is required" if path.blank?
|
70
|
+
path = Mapper.normalize_path(path)
|
71
|
+
|
72
|
+
if path.match(':controller')
|
73
|
+
raise ArgumentError, ":controller segment is not allowed within a namespace block" if @scope[:module]
|
74
|
+
|
75
|
+
# Add a default constraint for :controller path segments that matches namespaced
|
76
|
+
# controllers with default routes like :controller/:action/:id(.:format), e.g:
|
77
|
+
# GET /admin/products/show/1
|
78
|
+
# => { :controller => 'admin/products', :action => 'show', :id => '1' }
|
79
|
+
@options[:controller] ||= /.+?/
|
80
|
+
end
|
81
|
+
|
82
|
+
# Add a constraint for wildcard route to make it non-greedy and match the
|
83
|
+
# optional format part of the route by default
|
84
|
+
if path.match(WILDCARD_PATH) && @options[:format] != false
|
85
|
+
@options[$1.to_sym] ||= /.+?/
|
86
|
+
end
|
87
|
+
|
88
|
+
if @options[:format] == false
|
89
|
+
@options.delete(:format)
|
90
|
+
path
|
91
|
+
elsif path.include?(":format") || path.end_with?('/')
|
92
|
+
path
|
93
|
+
elsif @options[:format] == true
|
94
|
+
"#{path}.:format"
|
95
|
+
else
|
96
|
+
# Originally this was "#{path}(.:format)" but rails 2.3
|
97
|
+
# automatically wraps the format in parens and gets angry if you do
|
98
|
+
# it explicitly.
|
99
|
+
# This block wasn't combined with the block above just to add this
|
100
|
+
# comment, it took me a while to figure this out.
|
101
|
+
"#{path}.:format"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def conditions
|
106
|
+
{ :path_info => @path }.merge(constraints).merge(request_method_condition)
|
107
|
+
end
|
108
|
+
|
109
|
+
def requirements
|
110
|
+
@requirements ||= (@options[:constraints].is_a?(Hash) ? @options[:constraints] : {}).tap do |requirements|
|
111
|
+
requirements.reverse_merge!(@scope[:constraints]) if @scope[:constraints]
|
112
|
+
@options.each { |k, v| requirements[k] = v if v.is_a?(Regexp) }
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def defaults
|
117
|
+
@defaults ||= (@options[:defaults] || {}).tap do |defaults|
|
118
|
+
defaults.reverse_merge!(@scope[:defaults]) if @scope[:defaults]
|
119
|
+
@options.each { |k, v| defaults[k] = v unless v.is_a?(Regexp) || IGNORE_OPTIONS.include?(k.to_sym) }
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def default_controller_and_action
|
124
|
+
if to.respond_to?(:call)
|
125
|
+
{ }
|
126
|
+
else
|
127
|
+
if to.is_a?(String)
|
128
|
+
controller, action = to.split('#')
|
129
|
+
elsif to.is_a?(Symbol)
|
130
|
+
action = to.to_s
|
131
|
+
end
|
132
|
+
|
133
|
+
controller ||= default_controller
|
134
|
+
action ||= default_action
|
135
|
+
|
136
|
+
unless controller.is_a?(Regexp)
|
137
|
+
controller = [@scope[:module], controller].compact.join("/").presence
|
138
|
+
end
|
139
|
+
|
140
|
+
if controller.is_a?(String) && controller =~ %r{\A/}
|
141
|
+
raise ArgumentError, "controller name should not start with a slash"
|
142
|
+
end
|
143
|
+
|
144
|
+
controller = controller.to_s unless controller.is_a?(Regexp)
|
145
|
+
action = action.to_s unless action.is_a?(Regexp)
|
146
|
+
|
147
|
+
if controller.blank? && segment_keys.exclude?("controller")
|
148
|
+
raise ArgumentError, "missing :controller"
|
149
|
+
end
|
150
|
+
|
151
|
+
if action.blank? && segment_keys.exclude?("action")
|
152
|
+
raise ArgumentError, "missing :action"
|
153
|
+
end
|
154
|
+
|
155
|
+
hash = {}
|
156
|
+
hash[:controller] = controller unless controller.blank?
|
157
|
+
hash[:action] = action unless action.blank?
|
158
|
+
hash
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def blocks
|
163
|
+
constraints = @options[:constraints]
|
164
|
+
if constraints.present? && !constraints.is_a?(Hash)
|
165
|
+
[constraints]
|
166
|
+
else
|
167
|
+
@scope[:blocks] || []
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def constraints
|
172
|
+
@constraints ||= requirements.reject { |k, v| segment_keys.include?(k.to_s) || k == :controller }
|
173
|
+
end
|
174
|
+
|
175
|
+
def request_method_condition
|
176
|
+
if via = @options[:via]
|
177
|
+
list = Array(via).map { |m| m.to_s.dasherize.upcase }
|
178
|
+
{ :request_method => list }
|
179
|
+
else
|
180
|
+
{ }
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def segment_keys
|
185
|
+
@segment_keys ||= Journey::Path::Pattern.new(
|
186
|
+
Journey::Router::Strexp.compile(@path, requirements, SEPARATORS)
|
187
|
+
).names
|
188
|
+
end
|
189
|
+
|
190
|
+
def to
|
191
|
+
@options[:to]
|
192
|
+
end
|
193
|
+
|
194
|
+
def default_controller
|
195
|
+
@options[:controller] || @scope[:controller]
|
196
|
+
end
|
197
|
+
|
198
|
+
def default_action
|
199
|
+
@options[:action] || @scope[:action]
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
module Base
|
204
|
+
# You can specify what Rails should route "/" to with the root method:
|
205
|
+
#
|
206
|
+
# root :to => 'pages#main'
|
207
|
+
#
|
208
|
+
# For options, see +match+, as +root+ uses it internally.
|
209
|
+
#
|
210
|
+
# You should put the root route at the top of <tt>config/routes.rb</tt>,
|
211
|
+
# because this means it will be matched first. As this is the most popular route
|
212
|
+
# of most Rails applications, this is beneficial.
|
213
|
+
def root(options = {})
|
214
|
+
match '/', { :as => :root }.merge(options)
|
215
|
+
end
|
216
|
+
|
217
|
+
# Matches a url pattern to one or more routes. Any symbols in a pattern
|
218
|
+
# are interpreted as url query parameters and thus available as +params+
|
219
|
+
# in an action:
|
220
|
+
#
|
221
|
+
# # sets :controller, :action and :id in params
|
222
|
+
# match ':controller/:action/:id'
|
223
|
+
#
|
224
|
+
# Two of these symbols are special, +:controller+ maps to the controller
|
225
|
+
# and +:action+ to the controller's action. A pattern can also map
|
226
|
+
# wildcard segments (globs) to params:
|
227
|
+
#
|
228
|
+
# match 'songs/*category/:title' => 'songs#show'
|
229
|
+
#
|
230
|
+
# # 'songs/rock/classic/stairway-to-heaven' sets
|
231
|
+
# # params[:category] = 'rock/classic'
|
232
|
+
# # params[:title] = 'stairway-to-heaven'
|
233
|
+
#
|
234
|
+
# When a pattern points to an internal route, the route's +:action+ and
|
235
|
+
# +:controller+ should be set in options or hash shorthand. Examples:
|
236
|
+
#
|
237
|
+
# match 'photos/:id' => 'photos#show'
|
238
|
+
# match 'photos/:id', :to => 'photos#show'
|
239
|
+
# match 'photos/:id', :controller => 'photos', :action => 'show'
|
240
|
+
#
|
241
|
+
# A pattern can also point to a +Rack+ endpoint i.e. anything that
|
242
|
+
# responds to +call+:
|
243
|
+
#
|
244
|
+
# match 'photos/:id' => lambda {|hash| [200, {}, "Coming soon"] }
|
245
|
+
# match 'photos/:id' => PhotoRackApp
|
246
|
+
# # Yes, controller actions are just rack endpoints
|
247
|
+
# match 'photos/:id' => PhotosController.action(:show)
|
248
|
+
#
|
249
|
+
# === Options
|
250
|
+
#
|
251
|
+
# Any options not seen here are passed on as params with the url.
|
252
|
+
#
|
253
|
+
# [:controller]
|
254
|
+
# The route's controller.
|
255
|
+
#
|
256
|
+
# [:action]
|
257
|
+
# The route's action.
|
258
|
+
#
|
259
|
+
# [:path]
|
260
|
+
# The path prefix for the routes.
|
261
|
+
#
|
262
|
+
# [:module]
|
263
|
+
# The namespace for :controller.
|
264
|
+
#
|
265
|
+
# match 'path' => 'c#a', :module => 'sekret', :controller => 'posts'
|
266
|
+
# #=> Sekret::PostsController
|
267
|
+
#
|
268
|
+
# See <tt>Scoping#namespace</tt> for its scope equivalent.
|
269
|
+
#
|
270
|
+
# [:as]
|
271
|
+
# The name used to generate routing helpers.
|
272
|
+
#
|
273
|
+
# [:via]
|
274
|
+
# Allowed HTTP verb(s) for route.
|
275
|
+
#
|
276
|
+
# match 'path' => 'c#a', :via => :get
|
277
|
+
# match 'path' => 'c#a', :via => [:get, :post]
|
278
|
+
#
|
279
|
+
# [:to]
|
280
|
+
# Points to a +Rack+ endpoint. Can be an object that responds to
|
281
|
+
# +call+ or a string representing a controller's action.
|
282
|
+
#
|
283
|
+
# match 'path', :to => 'controller#action'
|
284
|
+
# match 'path', :to => lambda { |env| [200, {}, "Success!"] }
|
285
|
+
# match 'path', :to => RackApp
|
286
|
+
#
|
287
|
+
# [:on]
|
288
|
+
# Shorthand for wrapping routes in a specific RESTful context. Valid
|
289
|
+
# values are +:member+, +:collection+, and +:new+. Only use within
|
290
|
+
# <tt>resource(s)</tt> block. For example:
|
291
|
+
#
|
292
|
+
# resource :bar do
|
293
|
+
# match 'foo' => 'c#a', :on => :member, :via => [:get, :post]
|
294
|
+
# end
|
295
|
+
#
|
296
|
+
# Is equivalent to:
|
297
|
+
#
|
298
|
+
# resource :bar do
|
299
|
+
# member do
|
300
|
+
# match 'foo' => 'c#a', :via => [:get, :post]
|
301
|
+
# end
|
302
|
+
# end
|
303
|
+
#
|
304
|
+
# [:constraints]
|
305
|
+
# Constrains parameters with a hash of regular expressions or an
|
306
|
+
# object that responds to <tt>matches?</tt>
|
307
|
+
#
|
308
|
+
# match 'path/:id', :constraints => { :id => /[A-Z]\d{5}/ }
|
309
|
+
#
|
310
|
+
# class Blacklist
|
311
|
+
# def matches?(request) request.remote_ip == '1.2.3.4' end
|
312
|
+
# end
|
313
|
+
# match 'path' => 'c#a', :constraints => Blacklist.new
|
314
|
+
#
|
315
|
+
# See <tt>Scoping#constraints</tt> for more examples with its scope
|
316
|
+
# equivalent.
|
317
|
+
#
|
318
|
+
# [:defaults]
|
319
|
+
# Sets defaults for parameters
|
320
|
+
#
|
321
|
+
# # Sets params[:format] to 'jpg' by default
|
322
|
+
# match 'path' => 'c#a', :defaults => { :format => 'jpg' }
|
323
|
+
#
|
324
|
+
# See <tt>Scoping#defaults</tt> for its scope equivalent.
|
325
|
+
#
|
326
|
+
# [:anchor]
|
327
|
+
# Boolean to anchor a <tt>match</tt> pattern. Default is true. When set to
|
328
|
+
# false, the pattern matches any request prefixed with the given path.
|
329
|
+
#
|
330
|
+
# # Matches any request starting with 'path'
|
331
|
+
# match 'path' => 'c#a', :anchor => false
|
332
|
+
def match(path, options=nil)
|
333
|
+
end
|
334
|
+
|
335
|
+
def default_url_options=(options)
|
336
|
+
@set.default_url_options = options
|
337
|
+
end
|
338
|
+
alias_method :default_url_options, :default_url_options=
|
339
|
+
|
340
|
+
def with_default_scope(scope, &block)
|
341
|
+
scope(scope) do
|
342
|
+
instance_exec(&block)
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
module HttpHelpers
|
348
|
+
# Define a route that only recognizes HTTP GET.
|
349
|
+
# For supported arguments, see <tt>Base#match</tt>.
|
350
|
+
#
|
351
|
+
# Example:
|
352
|
+
#
|
353
|
+
# get 'bacon', :to => 'food#bacon'
|
354
|
+
def get(*args, &block)
|
355
|
+
map_method(:get, *args, &block)
|
356
|
+
end
|
357
|
+
|
358
|
+
# Define a route that only recognizes HTTP POST.
|
359
|
+
# For supported arguments, see <tt>Base#match</tt>.
|
360
|
+
#
|
361
|
+
# Example:
|
362
|
+
#
|
363
|
+
# post 'bacon', :to => 'food#bacon'
|
364
|
+
def post(*args, &block)
|
365
|
+
map_method(:post, *args, &block)
|
366
|
+
end
|
367
|
+
|
368
|
+
# Define a route that only recognizes HTTP PUT.
|
369
|
+
# For supported arguments, see <tt>Base#match</tt>.
|
370
|
+
#
|
371
|
+
# Example:
|
372
|
+
#
|
373
|
+
# put 'bacon', :to => 'food#bacon'
|
374
|
+
def put(*args, &block)
|
375
|
+
map_method(:put, *args, &block)
|
376
|
+
end
|
377
|
+
|
378
|
+
# Define a route that only recognizes HTTP PUT.
|
379
|
+
# For supported arguments, see <tt>Base#match</tt>.
|
380
|
+
#
|
381
|
+
# Example:
|
382
|
+
#
|
383
|
+
# delete 'broccoli', :to => 'food#broccoli'
|
384
|
+
def delete(*args, &block)
|
385
|
+
map_method(:delete, *args, &block)
|
386
|
+
end
|
387
|
+
|
388
|
+
private
|
389
|
+
def map_method(method, *args, &block)
|
390
|
+
options = args.extract_options!
|
391
|
+
options[:via] = method
|
392
|
+
args.push(options)
|
393
|
+
match(*args, &block)
|
394
|
+
self
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
# You may wish to organize groups of controllers under a namespace.
|
399
|
+
# Most commonly, you might group a number of administrative controllers
|
400
|
+
# under an +admin+ namespace. You would place these controllers under
|
401
|
+
# the <tt>app/controllers/admin</tt> directory, and you can group them
|
402
|
+
# together in your router:
|
403
|
+
#
|
404
|
+
# namespace "admin" do
|
405
|
+
# resources :posts, :comments
|
406
|
+
# end
|
407
|
+
#
|
408
|
+
# This will create a number of routes for each of the posts and comments
|
409
|
+
# controller. For <tt>Admin::PostsController</tt>, Rails will create:
|
410
|
+
#
|
411
|
+
# GET /admin/posts
|
412
|
+
# GET /admin/posts/new
|
413
|
+
# POST /admin/posts
|
414
|
+
# GET /admin/posts/1
|
415
|
+
# GET /admin/posts/1/edit
|
416
|
+
# PUT /admin/posts/1
|
417
|
+
# DELETE /admin/posts/1
|
418
|
+
#
|
419
|
+
# If you want to route /posts (without the prefix /admin) to
|
420
|
+
# <tt>Admin::PostsController</tt>, you could use
|
421
|
+
#
|
422
|
+
# scope :module => "admin" do
|
423
|
+
# resources :posts
|
424
|
+
# end
|
425
|
+
#
|
426
|
+
# or, for a single case
|
427
|
+
#
|
428
|
+
# resources :posts, :module => "admin"
|
429
|
+
#
|
430
|
+
# If you want to route /admin/posts to +PostsController+
|
431
|
+
# (without the Admin:: module prefix), you could use
|
432
|
+
#
|
433
|
+
# scope "/admin" do
|
434
|
+
# resources :posts
|
435
|
+
# end
|
436
|
+
#
|
437
|
+
# or, for a single case
|
438
|
+
#
|
439
|
+
# resources :posts, :path => "/admin/posts"
|
440
|
+
#
|
441
|
+
# In each of these cases, the named routes remain the same as if you did
|
442
|
+
# not use scope. In the last case, the following paths map to
|
443
|
+
# +PostsController+:
|
444
|
+
#
|
445
|
+
# GET /admin/posts
|
446
|
+
# GET /admin/posts/new
|
447
|
+
# POST /admin/posts
|
448
|
+
# GET /admin/posts/1
|
449
|
+
# GET /admin/posts/1/edit
|
450
|
+
# PUT /admin/posts/1
|
451
|
+
# DELETE /admin/posts/1
|
452
|
+
module Scoping
|
453
|
+
# Scopes a set of routes to the given default options.
|
454
|
+
#
|
455
|
+
# Take the following route definition as an example:
|
456
|
+
#
|
457
|
+
# scope :path => ":account_id", :as => "account" do
|
458
|
+
# resources :projects
|
459
|
+
# end
|
460
|
+
#
|
461
|
+
# This generates helpers such as +account_projects_path+, just like +resources+ does.
|
462
|
+
# The difference here being that the routes generated are like /:account_id/projects,
|
463
|
+
# rather than /accounts/:account_id/projects.
|
464
|
+
#
|
465
|
+
# === Options
|
466
|
+
#
|
467
|
+
# Takes same options as <tt>Base#match</tt> and <tt>Resources#resources</tt>.
|
468
|
+
#
|
469
|
+
# === Examples
|
470
|
+
#
|
471
|
+
# # route /posts (without the prefix /admin) to <tt>Admin::PostsController</tt>
|
472
|
+
# scope :module => "admin" do
|
473
|
+
# resources :posts
|
474
|
+
# end
|
475
|
+
#
|
476
|
+
# # prefix the posts resource's requests with '/admin'
|
477
|
+
# scope :path => "/admin" do
|
478
|
+
# resources :posts
|
479
|
+
# end
|
480
|
+
#
|
481
|
+
# # prefix the routing helper name: +sekret_posts_path+ instead of +posts_path+
|
482
|
+
# scope :as => "sekret" do
|
483
|
+
# resources :posts
|
484
|
+
# end
|
485
|
+
def scope(*args)
|
486
|
+
options = args.extract_options!
|
487
|
+
options = options.dup
|
488
|
+
|
489
|
+
options[:path] = args.first if args.first.is_a?(String)
|
490
|
+
recover = {}
|
491
|
+
|
492
|
+
options[:constraints] ||= {}
|
493
|
+
unless options[:constraints].is_a?(Hash)
|
494
|
+
block, options[:constraints] = options[:constraints], {}
|
495
|
+
end
|
496
|
+
|
497
|
+
scope_options.each do |option|
|
498
|
+
if value = options.delete(option)
|
499
|
+
recover[option] = @scope[option]
|
500
|
+
@scope[option] = send("merge_#{option}_scope", @scope[option], value)
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
recover[:block] = @scope[:blocks]
|
505
|
+
@scope[:blocks] = merge_blocks_scope(@scope[:blocks], block)
|
506
|
+
|
507
|
+
recover[:options] = @scope[:options]
|
508
|
+
@scope[:options] = merge_options_scope(@scope[:options], options)
|
509
|
+
|
510
|
+
yield
|
511
|
+
self
|
512
|
+
ensure
|
513
|
+
scope_options.each do |option|
|
514
|
+
@scope[option] = recover[option] if recover.has_key?(option)
|
515
|
+
end
|
516
|
+
|
517
|
+
@scope[:options] = recover[:options]
|
518
|
+
@scope[:blocks] = recover[:block]
|
519
|
+
end
|
520
|
+
|
521
|
+
# Scopes routes to a specific controller
|
522
|
+
#
|
523
|
+
# Example:
|
524
|
+
# controller "food" do
|
525
|
+
# match "bacon", :action => "bacon"
|
526
|
+
# end
|
527
|
+
def controller(controller, options={})
|
528
|
+
options[:controller] = controller
|
529
|
+
scope(options) { yield }
|
530
|
+
end
|
531
|
+
|
532
|
+
# Scopes routes to a specific namespace. For example:
|
533
|
+
#
|
534
|
+
# namespace :admin do
|
535
|
+
# resources :posts
|
536
|
+
# end
|
537
|
+
#
|
538
|
+
# This generates the following routes:
|
539
|
+
#
|
540
|
+
# admin_posts GET /admin/posts(.:format) admin/posts#index
|
541
|
+
# admin_posts POST /admin/posts(.:format) admin/posts#create
|
542
|
+
# new_admin_post GET /admin/posts/new(.:format) admin/posts#new
|
543
|
+
# edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit
|
544
|
+
# admin_post GET /admin/posts/:id(.:format) admin/posts#show
|
545
|
+
# admin_post PUT /admin/posts/:id(.:format) admin/posts#update
|
546
|
+
# admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy
|
547
|
+
#
|
548
|
+
# === Options
|
549
|
+
#
|
550
|
+
# The +:path+, +:as+, +:module+, +:shallow_path+ and +:shallow_prefix+
|
551
|
+
# options all default to the name of the namespace.
|
552
|
+
#
|
553
|
+
# For options, see <tt>Base#match</tt>. For +:shallow_path+ option, see
|
554
|
+
# <tt>Resources#resources</tt>.
|
555
|
+
#
|
556
|
+
# === Examples
|
557
|
+
#
|
558
|
+
# # accessible through /sekret/posts rather than /admin/posts
|
559
|
+
# namespace :admin, :path => "sekret" do
|
560
|
+
# resources :posts
|
561
|
+
# end
|
562
|
+
#
|
563
|
+
# # maps to <tt>Sekret::PostsController</tt> rather than <tt>Admin::PostsController</tt>
|
564
|
+
# namespace :admin, :module => "sekret" do
|
565
|
+
# resources :posts
|
566
|
+
# end
|
567
|
+
#
|
568
|
+
# # generates +sekret_posts_path+ rather than +admin_posts_path+
|
569
|
+
# namespace :admin, :as => "sekret" do
|
570
|
+
# resources :posts
|
571
|
+
# end
|
572
|
+
def namespace(path, options = {})
|
573
|
+
path = path.to_s
|
574
|
+
options = { :path => path, :as => path, :module => path,
|
575
|
+
:shallow_path => path, :shallow_prefix => path }.merge!(options)
|
576
|
+
scope(options) { yield }
|
577
|
+
end
|
578
|
+
|
579
|
+
# === Parameter Restriction
|
580
|
+
# Allows you to constrain the nested routes based on a set of rules.
|
581
|
+
# For instance, in order to change the routes to allow for a dot character in the +id+ parameter:
|
582
|
+
#
|
583
|
+
# constraints(:id => /\d+\.\d+/) do
|
584
|
+
# resources :posts
|
585
|
+
# end
|
586
|
+
#
|
587
|
+
# Now routes such as +/posts/1+ will no longer be valid, but +/posts/1.1+ will be.
|
588
|
+
# The +id+ parameter must match the constraint passed in for this example.
|
589
|
+
#
|
590
|
+
# You may use this to also restrict other parameters:
|
591
|
+
#
|
592
|
+
# resources :posts do
|
593
|
+
# constraints(:post_id => /\d+\.\d+/) do
|
594
|
+
# resources :comments
|
595
|
+
# end
|
596
|
+
# end
|
597
|
+
#
|
598
|
+
# === Restricting based on IP
|
599
|
+
#
|
600
|
+
# Routes can also be constrained to an IP or a certain range of IP addresses:
|
601
|
+
#
|
602
|
+
# constraints(:ip => /192.168.\d+.\d+/) do
|
603
|
+
# resources :posts
|
604
|
+
# end
|
605
|
+
#
|
606
|
+
# Any user connecting from the 192.168.* range will be able to see this resource,
|
607
|
+
# where as any user connecting outside of this range will be told there is no such route.
|
608
|
+
#
|
609
|
+
# === Dynamic request matching
|
610
|
+
#
|
611
|
+
# Requests to routes can be constrained based on specific criteria:
|
612
|
+
#
|
613
|
+
# constraints(lambda { |req| req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do
|
614
|
+
# resources :iphones
|
615
|
+
# end
|
616
|
+
#
|
617
|
+
# You are able to move this logic out into a class if it is too complex for routes.
|
618
|
+
# This class must have a +matches?+ method defined on it which either returns +true+
|
619
|
+
# if the user should be given access to that route, or +false+ if the user should not.
|
620
|
+
#
|
621
|
+
# class Iphone
|
622
|
+
# def self.matches?(request)
|
623
|
+
# request.env["HTTP_USER_AGENT"] =~ /iPhone/
|
624
|
+
# end
|
625
|
+
# end
|
626
|
+
#
|
627
|
+
# An expected place for this code would be +lib/constraints+.
|
628
|
+
#
|
629
|
+
# This class is then used like this:
|
630
|
+
#
|
631
|
+
# constraints(Iphone) do
|
632
|
+
# resources :iphones
|
633
|
+
# end
|
634
|
+
def constraints(constraints = {})
|
635
|
+
scope(:constraints => constraints) { yield }
|
636
|
+
end
|
637
|
+
|
638
|
+
# Allows you to set default parameters for a route, such as this:
|
639
|
+
# defaults :id => 'home' do
|
640
|
+
# match 'scoped_pages/(:id)', :to => 'pages#show'
|
641
|
+
# end
|
642
|
+
# Using this, the +:id+ parameter here will default to 'home'.
|
643
|
+
def defaults(defaults = {})
|
644
|
+
scope(:defaults => defaults) { yield }
|
645
|
+
end
|
646
|
+
|
647
|
+
private
|
648
|
+
def scope_options #:nodoc:
|
649
|
+
@scope_options ||= private_methods.grep(/^merge_(.+)_scope$/) { $1.to_sym }
|
650
|
+
end
|
651
|
+
|
652
|
+
def merge_path_scope(parent, child) #:nodoc:
|
653
|
+
Mapper.normalize_path("#{parent}/#{child}")
|
654
|
+
end
|
655
|
+
|
656
|
+
def merge_shallow_path_scope(parent, child) #:nodoc:
|
657
|
+
Mapper.normalize_path("#{parent}/#{child}")
|
658
|
+
end
|
659
|
+
|
660
|
+
def merge_as_scope(parent, child) #:nodoc:
|
661
|
+
parent ? "#{parent}_#{child}" : child
|
662
|
+
end
|
663
|
+
|
664
|
+
def merge_shallow_prefix_scope(parent, child) #:nodoc:
|
665
|
+
parent ? "#{parent}_#{child}" : child
|
666
|
+
end
|
667
|
+
|
668
|
+
def merge_module_scope(parent, child) #:nodoc:
|
669
|
+
parent ? "#{parent}/#{child}" : child
|
670
|
+
end
|
671
|
+
|
672
|
+
def merge_controller_scope(parent, child) #:nodoc:
|
673
|
+
child
|
674
|
+
end
|
675
|
+
|
676
|
+
def merge_path_names_scope(parent, child) #:nodoc:
|
677
|
+
merge_options_scope(parent, child)
|
678
|
+
end
|
679
|
+
|
680
|
+
def merge_constraints_scope(parent, child) #:nodoc:
|
681
|
+
merge_options_scope(parent, child)
|
682
|
+
end
|
683
|
+
|
684
|
+
def merge_defaults_scope(parent, child) #:nodoc:
|
685
|
+
merge_options_scope(parent, child)
|
686
|
+
end
|
687
|
+
|
688
|
+
def merge_blocks_scope(parent, child) #:nodoc:
|
689
|
+
merged = parent ? parent.dup : []
|
690
|
+
merged << child if child
|
691
|
+
merged
|
692
|
+
end
|
693
|
+
|
694
|
+
def merge_options_scope(parent, child) #:nodoc:
|
695
|
+
(parent || {}).except(*override_keys(child)).merge(child)
|
696
|
+
end
|
697
|
+
|
698
|
+
def merge_shallow_scope(parent, child) #:nodoc:
|
699
|
+
child ? true : false
|
700
|
+
end
|
701
|
+
|
702
|
+
def override_keys(child) #:nodoc:
|
703
|
+
child.key?(:only) || child.key?(:except) ? [:only, :except] : []
|
704
|
+
end
|
705
|
+
end
|
706
|
+
|
707
|
+
# Resource routing allows you to quickly declare all of the common routes
|
708
|
+
# for a given resourceful controller. Instead of declaring separate routes
|
709
|
+
# for your +index+, +show+, +new+, +edit+, +create+, +update+ and +destroy+
|
710
|
+
# actions, a resourceful route declares them in a single line of code:
|
711
|
+
#
|
712
|
+
# resources :photos
|
713
|
+
#
|
714
|
+
# Sometimes, you have a resource that clients always look up without
|
715
|
+
# referencing an ID. A common example, /profile always shows the profile of
|
716
|
+
# the currently logged in user. In this case, you can use a singular resource
|
717
|
+
# to map /profile (rather than /profile/:id) to the show action.
|
718
|
+
#
|
719
|
+
# resource :profile
|
720
|
+
#
|
721
|
+
# It's common to have resources that are logically children of other
|
722
|
+
# resources:
|
723
|
+
#
|
724
|
+
# resources :magazines do
|
725
|
+
# resources :ads
|
726
|
+
# end
|
727
|
+
#
|
728
|
+
# You may wish to organize groups of controllers under a namespace. Most
|
729
|
+
# commonly, you might group a number of administrative controllers under
|
730
|
+
# an +admin+ namespace. You would place these controllers under the
|
731
|
+
# <tt>app/controllers/admin</tt> directory, and you can group them together
|
732
|
+
# in your router:
|
733
|
+
#
|
734
|
+
# namespace "admin" do
|
735
|
+
# resources :posts, :comments
|
736
|
+
# end
|
737
|
+
#
|
738
|
+
# By default the +:id+ parameter doesn't accept dots. If you need to
|
739
|
+
# use dots as part of the +:id+ parameter add a constraint which
|
740
|
+
# overrides this restriction, e.g:
|
741
|
+
#
|
742
|
+
# resources :articles, :id => /[^\/]+/
|
743
|
+
#
|
744
|
+
# This allows any character other than a slash as part of your +:id+.
|
745
|
+
#
|
746
|
+
module Resources
|
747
|
+
# CANONICAL_ACTIONS holds all actions that does not need a prefix or
|
748
|
+
# a path appended since they fit properly in their scope level.
|
749
|
+
VALID_ON_OPTIONS = [:new, :collection, :member]
|
750
|
+
RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :format]
|
751
|
+
CANONICAL_ACTIONS = %w(index create new show update destroy)
|
752
|
+
|
753
|
+
class Resource #:nodoc:
|
754
|
+
attr_reader :controller, :path, :options
|
755
|
+
|
756
|
+
def initialize(entities, options = {})
|
757
|
+
@name = entities.to_s
|
758
|
+
@path = (options[:path] || @name).to_s
|
759
|
+
@controller = (options[:controller] || @name).to_s
|
760
|
+
@as = options[:as]
|
761
|
+
@options = options
|
762
|
+
end
|
763
|
+
|
764
|
+
def default_actions
|
765
|
+
[:index, :create, :new, :show, :update, :destroy, :edit]
|
766
|
+
end
|
767
|
+
|
768
|
+
def actions
|
769
|
+
if only = @options[:only]
|
770
|
+
Array(only).map(&:to_sym)
|
771
|
+
elsif except = @options[:except]
|
772
|
+
default_actions - Array(except).map(&:to_sym)
|
773
|
+
else
|
774
|
+
default_actions
|
775
|
+
end
|
776
|
+
end
|
777
|
+
|
778
|
+
def name
|
779
|
+
@as || @name
|
780
|
+
end
|
781
|
+
|
782
|
+
def plural
|
783
|
+
@plural ||= name.to_s
|
784
|
+
end
|
785
|
+
|
786
|
+
def singular
|
787
|
+
@singular ||= name.to_s.singularize
|
788
|
+
end
|
789
|
+
|
790
|
+
alias :member_name :singular
|
791
|
+
|
792
|
+
# Checks for uncountable plurals, and appends "_index" if the plural
|
793
|
+
# and singular form are the same.
|
794
|
+
def collection_name
|
795
|
+
singular == plural ? "#{plural}_index" : plural
|
796
|
+
end
|
797
|
+
|
798
|
+
def resource_scope
|
799
|
+
{ :controller => controller }
|
800
|
+
end
|
801
|
+
|
802
|
+
alias :collection_scope :path
|
803
|
+
|
804
|
+
def member_scope
|
805
|
+
"#{path}/:id"
|
806
|
+
end
|
807
|
+
|
808
|
+
def new_scope(new_path)
|
809
|
+
"#{path}/#{new_path}"
|
810
|
+
end
|
811
|
+
|
812
|
+
def nested_scope
|
813
|
+
"#{path}/:#{singular}_id"
|
814
|
+
end
|
815
|
+
|
816
|
+
end
|
817
|
+
|
818
|
+
class SingletonResource < Resource #:nodoc:
|
819
|
+
def initialize(entities, options)
|
820
|
+
super
|
821
|
+
@as = nil
|
822
|
+
@controller = (options[:controller] || plural).to_s
|
823
|
+
@as = options[:as]
|
824
|
+
end
|
825
|
+
|
826
|
+
def default_actions
|
827
|
+
[:show, :create, :update, :destroy, :new, :edit]
|
828
|
+
end
|
829
|
+
|
830
|
+
def plural
|
831
|
+
@plural ||= name.to_s.pluralize
|
832
|
+
end
|
833
|
+
|
834
|
+
def singular
|
835
|
+
@singular ||= name.to_s
|
836
|
+
end
|
837
|
+
|
838
|
+
alias :member_name :singular
|
839
|
+
alias :collection_name :singular
|
840
|
+
|
841
|
+
alias :member_scope :path
|
842
|
+
alias :nested_scope :path
|
843
|
+
end
|
844
|
+
|
845
|
+
def resources_path_names(options)
|
846
|
+
@scope[:path_names].merge!(options)
|
847
|
+
end
|
848
|
+
|
849
|
+
# Sometimes, you have a resource that clients always look up without
|
850
|
+
# referencing an ID. A common example, /profile always shows the
|
851
|
+
# profile of the currently logged in user. In this case, you can use
|
852
|
+
# a singular resource to map /profile (rather than /profile/:id) to
|
853
|
+
# the show action:
|
854
|
+
#
|
855
|
+
# resource :geocoder
|
856
|
+
#
|
857
|
+
# creates six different routes in your application, all mapping to
|
858
|
+
# the +GeoCoders+ controller (note that the controller is named after
|
859
|
+
# the plural):
|
860
|
+
#
|
861
|
+
# GET /geocoder/new
|
862
|
+
# POST /geocoder
|
863
|
+
# GET /geocoder
|
864
|
+
# GET /geocoder/edit
|
865
|
+
# PUT /geocoder
|
866
|
+
# DELETE /geocoder
|
867
|
+
#
|
868
|
+
# === Options
|
869
|
+
# Takes same options as +resources+.
|
870
|
+
def resource(*resources, &block)
|
871
|
+
options = resources.extract_options!.dup
|
872
|
+
|
873
|
+
if apply_common_behavior_for(:resource, resources, options, &block)
|
874
|
+
return self
|
875
|
+
end
|
876
|
+
|
877
|
+
resource_scope(:resource, SingletonResource.new(resources.pop, options)) do
|
878
|
+
yield if block_given?
|
879
|
+
|
880
|
+
new do
|
881
|
+
get :new
|
882
|
+
end if parent_resource.actions.include?(:new)
|
883
|
+
|
884
|
+
member do
|
885
|
+
get :edit if parent_resource.actions.include?(:edit)
|
886
|
+
get :show if parent_resource.actions.include?(:show)
|
887
|
+
put :update if parent_resource.actions.include?(:update)
|
888
|
+
delete :destroy if parent_resource.actions.include?(:destroy)
|
889
|
+
end
|
890
|
+
|
891
|
+
collection do
|
892
|
+
post :create
|
893
|
+
end if parent_resource.actions.include?(:create)
|
894
|
+
end
|
895
|
+
|
896
|
+
self
|
897
|
+
end
|
898
|
+
|
899
|
+
# In Rails, a resourceful route provides a mapping between HTTP verbs
|
900
|
+
# and URLs and controller actions. By convention, each action also maps
|
901
|
+
# to particular CRUD operations in a database. A single entry in the
|
902
|
+
# routing file, such as
|
903
|
+
#
|
904
|
+
# resources :photos
|
905
|
+
#
|
906
|
+
# creates seven different routes in your application, all mapping to
|
907
|
+
# the +Photos+ controller:
|
908
|
+
#
|
909
|
+
# GET /photos
|
910
|
+
# GET /photos/new
|
911
|
+
# POST /photos
|
912
|
+
# GET /photos/:id
|
913
|
+
# GET /photos/:id/edit
|
914
|
+
# PUT /photos/:id
|
915
|
+
# DELETE /photos/:id
|
916
|
+
#
|
917
|
+
# Resources can also be nested infinitely by using this block syntax:
|
918
|
+
#
|
919
|
+
# resources :photos do
|
920
|
+
# resources :comments
|
921
|
+
# end
|
922
|
+
#
|
923
|
+
# This generates the following comments routes:
|
924
|
+
#
|
925
|
+
# GET /photos/:photo_id/comments
|
926
|
+
# GET /photos/:photo_id/comments/new
|
927
|
+
# POST /photos/:photo_id/comments
|
928
|
+
# GET /photos/:photo_id/comments/:id
|
929
|
+
# GET /photos/:photo_id/comments/:id/edit
|
930
|
+
# PUT /photos/:photo_id/comments/:id
|
931
|
+
# DELETE /photos/:photo_id/comments/:id
|
932
|
+
#
|
933
|
+
# === Options
|
934
|
+
# Takes same options as <tt>Base#match</tt> as well as:
|
935
|
+
#
|
936
|
+
# [:path_names]
|
937
|
+
# Allows you to change the segment component of the +edit+ and +new+ actions.
|
938
|
+
# Actions not specified are not changed.
|
939
|
+
#
|
940
|
+
# resources :posts, :path_names => { :new => "brand_new" }
|
941
|
+
#
|
942
|
+
# The above example will now change /posts/new to /posts/brand_new
|
943
|
+
#
|
944
|
+
# [:path]
|
945
|
+
# Allows you to change the path prefix for the resource.
|
946
|
+
#
|
947
|
+
# resources :posts, :path => 'postings'
|
948
|
+
#
|
949
|
+
# The resource and all segments will now route to /postings instead of /posts
|
950
|
+
#
|
951
|
+
# [:only]
|
952
|
+
# Only generate routes for the given actions.
|
953
|
+
#
|
954
|
+
# resources :cows, :only => :show
|
955
|
+
# resources :cows, :only => [:show, :index]
|
956
|
+
#
|
957
|
+
# [:except]
|
958
|
+
# Generate all routes except for the given actions.
|
959
|
+
#
|
960
|
+
# resources :cows, :except => :show
|
961
|
+
# resources :cows, :except => [:show, :index]
|
962
|
+
#
|
963
|
+
# [:shallow]
|
964
|
+
# Generates shallow routes for nested resource(s). When placed on a parent resource,
|
965
|
+
# generates shallow routes for all nested resources.
|
966
|
+
#
|
967
|
+
# resources :posts, :shallow => true do
|
968
|
+
# resources :comments
|
969
|
+
# end
|
970
|
+
#
|
971
|
+
# Is the same as:
|
972
|
+
#
|
973
|
+
# resources :posts do
|
974
|
+
# resources :comments, :except => [:show, :edit, :update, :destroy]
|
975
|
+
# end
|
976
|
+
# resources :comments, :only => [:show, :edit, :update, :destroy]
|
977
|
+
#
|
978
|
+
# This allows URLs for resources that otherwise would be deeply nested such
|
979
|
+
# as a comment on a blog post like <tt>/posts/a-long-permalink/comments/1234</tt>
|
980
|
+
# to be shortened to just <tt>/comments/1234</tt>.
|
981
|
+
#
|
982
|
+
# [:shallow_path]
|
983
|
+
# Prefixes nested shallow routes with the specified path.
|
984
|
+
#
|
985
|
+
# scope :shallow_path => "sekret" do
|
986
|
+
# resources :posts do
|
987
|
+
# resources :comments, :shallow => true
|
988
|
+
# end
|
989
|
+
# end
|
990
|
+
#
|
991
|
+
# The +comments+ resource here will have the following routes generated for it:
|
992
|
+
#
|
993
|
+
# post_comments GET /posts/:post_id/comments(.:format)
|
994
|
+
# post_comments POST /posts/:post_id/comments(.:format)
|
995
|
+
# new_post_comment GET /posts/:post_id/comments/new(.:format)
|
996
|
+
# edit_comment GET /sekret/comments/:id/edit(.:format)
|
997
|
+
# comment GET /sekret/comments/:id(.:format)
|
998
|
+
# comment PUT /sekret/comments/:id(.:format)
|
999
|
+
# comment DELETE /sekret/comments/:id(.:format)
|
1000
|
+
#
|
1001
|
+
# === Examples
|
1002
|
+
#
|
1003
|
+
# # routes call <tt>Admin::PostsController</tt>
|
1004
|
+
# resources :posts, :module => "admin"
|
1005
|
+
#
|
1006
|
+
# # resource actions are at /admin/posts.
|
1007
|
+
# resources :posts, :path => "admin/posts"
|
1008
|
+
def resources(*resources, &block)
|
1009
|
+
options = resources.extract_options!.dup
|
1010
|
+
|
1011
|
+
if apply_common_behavior_for(:resources, resources, options, &block)
|
1012
|
+
return self
|
1013
|
+
end
|
1014
|
+
|
1015
|
+
resource_scope(:resources, Resource.new(resources.pop, options)) do
|
1016
|
+
yield if block_given?
|
1017
|
+
|
1018
|
+
collection do
|
1019
|
+
get :index if parent_resource.actions.include?(:index)
|
1020
|
+
post :create if parent_resource.actions.include?(:create)
|
1021
|
+
end
|
1022
|
+
|
1023
|
+
new do
|
1024
|
+
get :new
|
1025
|
+
end if parent_resource.actions.include?(:new)
|
1026
|
+
|
1027
|
+
member do
|
1028
|
+
get :edit if parent_resource.actions.include?(:edit)
|
1029
|
+
end
|
1030
|
+
|
1031
|
+
member do
|
1032
|
+
get :show if parent_resource.actions.include?(:show)
|
1033
|
+
end
|
1034
|
+
|
1035
|
+
member do
|
1036
|
+
put :update if parent_resource.actions.include?(:update)
|
1037
|
+
delete :destroy if parent_resource.actions.include?(:destroy)
|
1038
|
+
end
|
1039
|
+
end
|
1040
|
+
|
1041
|
+
self
|
1042
|
+
end
|
1043
|
+
|
1044
|
+
# To add a route to the collection:
|
1045
|
+
#
|
1046
|
+
# resources :photos do
|
1047
|
+
# collection do
|
1048
|
+
# get 'search'
|
1049
|
+
# end
|
1050
|
+
# end
|
1051
|
+
#
|
1052
|
+
# This will enable Rails to recognize paths such as <tt>/photos/search</tt>
|
1053
|
+
# with GET, and route to the search action of +PhotosController+. It will also
|
1054
|
+
# create the <tt>search_photos_url</tt> and <tt>search_photos_path</tt>
|
1055
|
+
# route helpers.
|
1056
|
+
def collection
|
1057
|
+
unless resource_scope?
|
1058
|
+
raise ArgumentError, "can't use collection outside resource(s) scope"
|
1059
|
+
end
|
1060
|
+
|
1061
|
+
with_scope_level(:collection) do
|
1062
|
+
scope(parent_resource.collection_scope) do
|
1063
|
+
yield
|
1064
|
+
end
|
1065
|
+
end
|
1066
|
+
end
|
1067
|
+
|
1068
|
+
# To add a member route, add a member block into the resource block:
|
1069
|
+
#
|
1070
|
+
# resources :photos do
|
1071
|
+
# member do
|
1072
|
+
# get 'preview'
|
1073
|
+
# end
|
1074
|
+
# end
|
1075
|
+
#
|
1076
|
+
# This will recognize <tt>/photos/1/preview</tt> with GET, and route to the
|
1077
|
+
# preview action of +PhotosController+. It will also create the
|
1078
|
+
# <tt>preview_photo_url</tt> and <tt>preview_photo_path</tt> helpers.
|
1079
|
+
def member
|
1080
|
+
unless resource_scope?
|
1081
|
+
raise ArgumentError, "can't use member outside resource(s) scope"
|
1082
|
+
end
|
1083
|
+
|
1084
|
+
with_scope_level(:member) do
|
1085
|
+
scope(parent_resource.member_scope) do
|
1086
|
+
yield
|
1087
|
+
end
|
1088
|
+
end
|
1089
|
+
end
|
1090
|
+
|
1091
|
+
def new
|
1092
|
+
unless resource_scope?
|
1093
|
+
raise ArgumentError, "can't use new outside resource(s) scope"
|
1094
|
+
end
|
1095
|
+
|
1096
|
+
with_scope_level(:new) do
|
1097
|
+
scope(parent_resource.new_scope(action_path(:new))) do
|
1098
|
+
yield
|
1099
|
+
end
|
1100
|
+
end
|
1101
|
+
end
|
1102
|
+
|
1103
|
+
def nested
|
1104
|
+
unless resource_scope?
|
1105
|
+
raise ArgumentError, "can't use nested outside resource(s) scope"
|
1106
|
+
end
|
1107
|
+
|
1108
|
+
with_scope_level(:nested) do
|
1109
|
+
if shallow?
|
1110
|
+
with_exclusive_scope do
|
1111
|
+
if @scope[:shallow_path].blank?
|
1112
|
+
scope(parent_resource.nested_scope, nested_options) { yield }
|
1113
|
+
else
|
1114
|
+
scope(@scope[:shallow_path], :as => @scope[:shallow_prefix]) do
|
1115
|
+
scope(parent_resource.nested_scope, nested_options) { yield }
|
1116
|
+
end
|
1117
|
+
end
|
1118
|
+
end
|
1119
|
+
else
|
1120
|
+
scope(parent_resource.nested_scope, nested_options) { yield }
|
1121
|
+
end
|
1122
|
+
end
|
1123
|
+
end
|
1124
|
+
|
1125
|
+
# See ActionDispatch::Routing::Mapper::Scoping#namespace
|
1126
|
+
def namespace(path, options = {})
|
1127
|
+
if resource_scope?
|
1128
|
+
nested { super }
|
1129
|
+
else
|
1130
|
+
super
|
1131
|
+
end
|
1132
|
+
end
|
1133
|
+
|
1134
|
+
def shallow
|
1135
|
+
scope(:shallow => true, :shallow_path => @scope[:path]) do
|
1136
|
+
yield
|
1137
|
+
end
|
1138
|
+
end
|
1139
|
+
|
1140
|
+
def shallow?
|
1141
|
+
parent_resource.instance_of?(Resource) && @scope[:shallow]
|
1142
|
+
end
|
1143
|
+
|
1144
|
+
def match(path, *rest)
|
1145
|
+
if rest.empty? && Hash === path
|
1146
|
+
options = path
|
1147
|
+
path, to = options.find { |name, value| name.is_a?(String) }
|
1148
|
+
options[:to] = to
|
1149
|
+
options.delete(path)
|
1150
|
+
paths = [path]
|
1151
|
+
else
|
1152
|
+
options = rest.pop || {}
|
1153
|
+
paths = [path] + rest
|
1154
|
+
end
|
1155
|
+
|
1156
|
+
path_without_format = path.to_s.sub(/\(\.:format\)$/, '')
|
1157
|
+
if using_match_shorthand?(path_without_format, options)
|
1158
|
+
options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1')
|
1159
|
+
end
|
1160
|
+
|
1161
|
+
options[:anchor] = true unless options.key?(:anchor)
|
1162
|
+
|
1163
|
+
if options[:on] && !VALID_ON_OPTIONS.include?(options[:on])
|
1164
|
+
raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
|
1165
|
+
end
|
1166
|
+
|
1167
|
+
if !options.key?(:format)
|
1168
|
+
options[:format] = false
|
1169
|
+
end
|
1170
|
+
|
1171
|
+
paths.each { |_path| decomposed_match(_path, options.dup) }
|
1172
|
+
self
|
1173
|
+
end
|
1174
|
+
|
1175
|
+
def using_match_shorthand?(path, options)
|
1176
|
+
path && (options[:to] || options[:action]).nil? && path =~ %r{/[\w/]+$}
|
1177
|
+
end
|
1178
|
+
|
1179
|
+
def decomposed_match(path, options) # :nodoc:
|
1180
|
+
if on = options.delete(:on)
|
1181
|
+
send(on) { decomposed_match(path, options) }
|
1182
|
+
else
|
1183
|
+
case @scope[:scope_level]
|
1184
|
+
when :resources
|
1185
|
+
nested { decomposed_match(path, options) }
|
1186
|
+
when :resource
|
1187
|
+
member { decomposed_match(path, options) }
|
1188
|
+
else
|
1189
|
+
add_route(path, options)
|
1190
|
+
end
|
1191
|
+
end
|
1192
|
+
end
|
1193
|
+
|
1194
|
+
def add_route(action, options) # :nodoc:
|
1195
|
+
path = path_for_action(action, options.delete(:path))
|
1196
|
+
action = action.to_s.dup
|
1197
|
+
|
1198
|
+
if action =~ /^[\w\/]+$/
|
1199
|
+
options[:action] ||= action unless action.include?("/")
|
1200
|
+
else
|
1201
|
+
action = nil
|
1202
|
+
end
|
1203
|
+
|
1204
|
+
if !options.fetch(:as, true)
|
1205
|
+
options.delete(:as)
|
1206
|
+
else
|
1207
|
+
options[:as] = name_for_action(options[:as], action)
|
1208
|
+
end
|
1209
|
+
|
1210
|
+
|
1211
|
+
mapping = Mapping.new(@set, @scope, path, options)
|
1212
|
+
#puts mapping.to_route.inspect
|
1213
|
+
conditions, requirements, defaults, as, anchor = mapping.to_route
|
1214
|
+
defaults[:conditions] ||= {}
|
1215
|
+
if conditions[:request_method]
|
1216
|
+
via = conditions.delete(:request_method).map { |v| v.downcase.to_sym }
|
1217
|
+
via = via.first if via.size == 1
|
1218
|
+
defaults[:conditions][:method] = via
|
1219
|
+
end
|
1220
|
+
@set.add_route(conditions, requirements, defaults, as, anchor)
|
1221
|
+
end
|
1222
|
+
|
1223
|
+
def root(options={})
|
1224
|
+
if @scope[:scope_level] == :resources
|
1225
|
+
with_scope_level(:root) do
|
1226
|
+
scope(parent_resource.path) do
|
1227
|
+
super(options)
|
1228
|
+
end
|
1229
|
+
end
|
1230
|
+
else
|
1231
|
+
super(options)
|
1232
|
+
end
|
1233
|
+
end
|
1234
|
+
|
1235
|
+
protected
|
1236
|
+
|
1237
|
+
def parent_resource #:nodoc:
|
1238
|
+
@scope[:scope_level_resource]
|
1239
|
+
end
|
1240
|
+
|
1241
|
+
def apply_common_behavior_for(method, resources, options, &block) #:nodoc:
|
1242
|
+
if resources.length > 1
|
1243
|
+
resources.each { |r| send(method, r, options, &block) }
|
1244
|
+
return true
|
1245
|
+
end
|
1246
|
+
|
1247
|
+
if resource_scope?
|
1248
|
+
nested { send(method, resources.pop, options, &block) }
|
1249
|
+
return true
|
1250
|
+
end
|
1251
|
+
|
1252
|
+
options.keys.each do |k|
|
1253
|
+
(options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
|
1254
|
+
end
|
1255
|
+
|
1256
|
+
scope_options = options.slice!(*RESOURCE_OPTIONS)
|
1257
|
+
unless scope_options.empty?
|
1258
|
+
scope(scope_options) do
|
1259
|
+
send(method, resources.pop, options, &block)
|
1260
|
+
end
|
1261
|
+
return true
|
1262
|
+
end
|
1263
|
+
|
1264
|
+
unless action_options?(options)
|
1265
|
+
options.merge!(scope_action_options) if scope_action_options?
|
1266
|
+
end
|
1267
|
+
|
1268
|
+
false
|
1269
|
+
end
|
1270
|
+
|
1271
|
+
def action_options?(options) #:nodoc:
|
1272
|
+
options[:only] || options[:except]
|
1273
|
+
end
|
1274
|
+
|
1275
|
+
def scope_action_options? #:nodoc:
|
1276
|
+
@scope[:options] && (@scope[:options][:only] || @scope[:options][:except])
|
1277
|
+
end
|
1278
|
+
|
1279
|
+
def scope_action_options #:nodoc:
|
1280
|
+
@scope[:options].slice(:only, :except)
|
1281
|
+
end
|
1282
|
+
|
1283
|
+
def resource_scope? #:nodoc:
|
1284
|
+
[:resource, :resources].include? @scope[:scope_level]
|
1285
|
+
end
|
1286
|
+
|
1287
|
+
def resource_method_scope? #:nodoc:
|
1288
|
+
[:collection, :member, :new].include? @scope[:scope_level]
|
1289
|
+
end
|
1290
|
+
|
1291
|
+
def with_exclusive_scope
|
1292
|
+
begin
|
1293
|
+
old_name_prefix, old_path = @scope[:as], @scope[:path]
|
1294
|
+
@scope[:as], @scope[:path] = nil, nil
|
1295
|
+
|
1296
|
+
with_scope_level(:exclusive) do
|
1297
|
+
yield
|
1298
|
+
end
|
1299
|
+
ensure
|
1300
|
+
@scope[:as], @scope[:path] = old_name_prefix, old_path
|
1301
|
+
end
|
1302
|
+
end
|
1303
|
+
|
1304
|
+
def with_scope_level(kind, resource = parent_resource)
|
1305
|
+
old, @scope[:scope_level] = @scope[:scope_level], kind
|
1306
|
+
old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
|
1307
|
+
yield
|
1308
|
+
ensure
|
1309
|
+
@scope[:scope_level] = old
|
1310
|
+
@scope[:scope_level_resource] = old_resource
|
1311
|
+
end
|
1312
|
+
|
1313
|
+
def resource_scope(kind, resource) #:nodoc:
|
1314
|
+
with_scope_level(kind, resource) do
|
1315
|
+
scope(parent_resource.resource_scope) do
|
1316
|
+
yield
|
1317
|
+
end
|
1318
|
+
end
|
1319
|
+
end
|
1320
|
+
|
1321
|
+
def nested_options #:nodoc:
|
1322
|
+
options = { :as => parent_resource.member_name }
|
1323
|
+
options[:constraints] = {
|
1324
|
+
:"#{parent_resource.singular}_id" => id_constraint
|
1325
|
+
} if id_constraint?
|
1326
|
+
|
1327
|
+
options
|
1328
|
+
end
|
1329
|
+
|
1330
|
+
def id_constraint? #:nodoc:
|
1331
|
+
@scope[:constraints] && @scope[:constraints][:id].is_a?(Regexp)
|
1332
|
+
end
|
1333
|
+
|
1334
|
+
def id_constraint #:nodoc:
|
1335
|
+
@scope[:constraints][:id]
|
1336
|
+
end
|
1337
|
+
|
1338
|
+
def canonical_action?(action, flag) #:nodoc:
|
1339
|
+
flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
|
1340
|
+
end
|
1341
|
+
|
1342
|
+
def shallow_scoping? #:nodoc:
|
1343
|
+
shallow? && @scope[:scope_level] == :member
|
1344
|
+
end
|
1345
|
+
|
1346
|
+
def path_for_action(action, path) #:nodoc:
|
1347
|
+
prefix = shallow_scoping? ?
|
1348
|
+
"#{@scope[:shallow_path]}/#{parent_resource.path}/:id" : @scope[:path]
|
1349
|
+
|
1350
|
+
path = if canonical_action?(action, path.blank?)
|
1351
|
+
prefix.to_s
|
1352
|
+
else
|
1353
|
+
"#{prefix}/#{action_path(action, path)}"
|
1354
|
+
end
|
1355
|
+
end
|
1356
|
+
|
1357
|
+
def action_path(name, path = nil) #:nodoc:
|
1358
|
+
# Ruby 1.8 can't transform empty strings to symbols
|
1359
|
+
name = name.to_sym if name.is_a?(String) && !name.empty?
|
1360
|
+
path || @scope[:path_names][name] || name.to_s
|
1361
|
+
end
|
1362
|
+
|
1363
|
+
def prefix_name_for_action(as, action) #:nodoc:
|
1364
|
+
if as
|
1365
|
+
as.to_s
|
1366
|
+
elsif !canonical_action?(action, @scope[:scope_level])
|
1367
|
+
action.to_s
|
1368
|
+
end
|
1369
|
+
end
|
1370
|
+
|
1371
|
+
def name_for_action(as, action) #:nodoc:
|
1372
|
+
prefix = prefix_name_for_action(as, action)
|
1373
|
+
prefix = Mapper.normalize_name(prefix) if prefix
|
1374
|
+
name_prefix = @scope[:as]
|
1375
|
+
|
1376
|
+
if parent_resource
|
1377
|
+
return nil unless as || action
|
1378
|
+
|
1379
|
+
collection_name = parent_resource.collection_name
|
1380
|
+
member_name = parent_resource.member_name
|
1381
|
+
end
|
1382
|
+
|
1383
|
+
name = case @scope[:scope_level]
|
1384
|
+
when :nested
|
1385
|
+
[name_prefix, prefix]
|
1386
|
+
when :collection
|
1387
|
+
[prefix, name_prefix, collection_name]
|
1388
|
+
when :new
|
1389
|
+
[prefix, :new, name_prefix, member_name]
|
1390
|
+
when :member
|
1391
|
+
[prefix, shallow_scoping? ? @scope[:shallow_prefix] : name_prefix, member_name]
|
1392
|
+
when :root
|
1393
|
+
[name_prefix, collection_name, prefix]
|
1394
|
+
else
|
1395
|
+
if as
|
1396
|
+
[name_prefix, member_name, prefix]
|
1397
|
+
else
|
1398
|
+
[]
|
1399
|
+
end
|
1400
|
+
end
|
1401
|
+
|
1402
|
+
if candidate = name.select(&:present?).join("_").presence
|
1403
|
+
# If a name was not explicitly given, we check if it is valid
|
1404
|
+
# and return nil in case it isn't. Otherwise, we pass the invalid name
|
1405
|
+
# forward so the underlying router engine treats it and raises an exception.
|
1406
|
+
if as.nil?
|
1407
|
+
candidate unless @set.named_route?(candidate) || candidate !~ /\A[_a-z]/i
|
1408
|
+
else
|
1409
|
+
candidate
|
1410
|
+
end
|
1411
|
+
end
|
1412
|
+
end
|
1413
|
+
end
|
1414
|
+
|
1415
|
+
include Base
|
1416
|
+
include HttpHelpers
|
1417
|
+
include Scoping
|
1418
|
+
include Resources
|
1419
|
+
end
|
1420
|
+
end
|