fake_rails3_routes 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|