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 ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in fake_rails3_routes.gemspec
4
+ gemspec
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