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 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