actionpack 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionpack might be problematic. Click here for more details.

Files changed (84) hide show
  1. data/CHANGELOG +66 -0
  2. data/README +94 -64
  3. data/install.rb +1 -20
  4. data/lib/action_controller.rb +15 -7
  5. data/lib/action_controller/assertions/action_pack_assertions.rb +56 -3
  6. data/lib/action_controller/base.rb +137 -64
  7. data/lib/action_controller/caching.rb +11 -11
  8. data/lib/action_controller/cgi_ext/cgi_ext.rb +2 -2
  9. data/lib/action_controller/cgi_ext/raw_post_data_fix.rb +4 -4
  10. data/lib/action_controller/cgi_process.rb +9 -1
  11. data/lib/action_controller/components.rb +73 -0
  12. data/lib/action_controller/cookies.rb +1 -1
  13. data/lib/action_controller/dependencies.rb +6 -1
  14. data/lib/action_controller/filters.rb +1 -1
  15. data/lib/action_controller/flash.rb +3 -3
  16. data/lib/action_controller/helpers.rb +17 -21
  17. data/lib/action_controller/layout.rb +2 -2
  18. data/lib/action_controller/request.rb +16 -6
  19. data/lib/action_controller/rescue.rb +15 -3
  20. data/lib/action_controller/routing.rb +304 -0
  21. data/lib/action_controller/scaffolding.rb +10 -12
  22. data/lib/action_controller/session/active_record_store.rb +4 -7
  23. data/lib/action_controller/session/mem_cache_store.rb +2 -2
  24. data/lib/action_controller/templates/rescues/_request_and_response.rhtml +9 -1
  25. data/lib/action_controller/templates/rescues/diagnostics.rhtml +1 -1
  26. data/lib/action_controller/templates/rescues/routing_error.rhtml +8 -0
  27. data/lib/action_controller/test_process.rb +29 -7
  28. data/lib/action_controller/url_rewriter.rb +28 -112
  29. data/lib/action_view.rb +1 -1
  30. data/lib/action_view/base.rb +44 -17
  31. data/lib/action_view/helpers/active_record_helper.rb +1 -1
  32. data/lib/action_view/helpers/asset_tag_helper.rb +59 -0
  33. data/lib/action_view/helpers/date_helper.rb +24 -13
  34. data/lib/action_view/helpers/form_helper.rb +6 -1
  35. data/lib/action_view/helpers/form_options_helper.rb +87 -9
  36. data/lib/action_view/helpers/form_tag_helper.rb +80 -0
  37. data/lib/action_view/helpers/tag_helper.rb +0 -23
  38. data/lib/action_view/helpers/text_helper.rb +26 -1
  39. data/lib/action_view/helpers/url_helper.rb +29 -35
  40. data/lib/action_view/partials.rb +2 -2
  41. data/lib/action_view/vendor/builder/xmlbase.rb +3 -3
  42. data/rakefile +5 -2
  43. data/test/abstract_unit.rb +3 -1
  44. data/test/controller/action_pack_assertions_test.rb +29 -1
  45. data/test/controller/active_record_assertions_test.rb +109 -101
  46. data/test/controller/base_tests.rb +72 -0
  47. data/test/controller/components_test.rb +74 -0
  48. data/test/controller/cookie_test.rb +0 -9
  49. data/test/controller/custom_handler_test.rb +33 -0
  50. data/test/controller/filters_test.rb +36 -0
  51. data/test/controller/helper_test.rb +27 -10
  52. data/test/controller/redirect_test.rb +23 -31
  53. data/test/controller/render_test.rb +81 -66
  54. data/test/controller/request_test.rb +22 -0
  55. data/test/controller/routing_tests.rb +490 -0
  56. data/test/controller/{url_test.rb → url_obsolete.rb} +24 -14
  57. data/test/controller/url_obsolete.rb.rej +747 -0
  58. data/test/fixtures/fun/games/hello_world.rhtml +1 -0
  59. data/test/fixtures/helpers/fun/games_helper.rb +3 -0
  60. data/test/template/asset_tag_helper_test.rb +45 -0
  61. data/test/template/form_options_helper_test.rb +161 -1
  62. data/test/template/form_tag_helper_test.rb +22 -0
  63. data/test/template/text_helper_test.rb +7 -0
  64. data/test/template/url_helper_test.rb +5 -2
  65. data/test/template/url_helper_test.rb.rej +105 -0
  66. metadata +32 -27
  67. data/lib/action_controller/support/binding_of_caller.rb +0 -83
  68. data/lib/action_controller/support/breakpoint.rb +0 -518
  69. data/lib/action_controller/support/class_attribute_accessors.rb +0 -57
  70. data/lib/action_controller/support/class_inheritable_attributes.rb +0 -117
  71. data/lib/action_controller/support/clean_logger.rb +0 -10
  72. data/lib/action_controller/support/core_ext.rb +0 -1
  73. data/lib/action_controller/support/core_ext/hash.rb +0 -5
  74. data/lib/action_controller/support/core_ext/hash/keys.rb +0 -35
  75. data/lib/action_controller/support/core_ext/numeric.rb +0 -7
  76. data/lib/action_controller/support/core_ext/numeric/bytes.rb +0 -33
  77. data/lib/action_controller/support/core_ext/numeric/time.rb +0 -59
  78. data/lib/action_controller/support/core_ext/object_and_class.rb +0 -24
  79. data/lib/action_controller/support/core_ext/string.rb +0 -5
  80. data/lib/action_controller/support/core_ext/string/inflections.rb +0 -45
  81. data/lib/action_controller/support/dependencies.rb +0 -63
  82. data/lib/action_controller/support/inflector.rb +0 -84
  83. data/lib/action_controller/support/misc.rb +0 -8
  84. data/lib/action_controller/support/module_attribute_accessors.rb +0 -57
@@ -1,7 +1,7 @@
1
1
  module ActionController #:nodoc:
2
2
  # Cookies are read and written through ActionController#cookies. The cookies being read is what was received along with the request,
3
3
  # the cookies being written is what will be sent out will the response. Cookies are read by value (so you won't get the cookie object
4
- # itself back -- just the value it holds). Examples for writting:
4
+ # itself back -- just the value it holds). Examples for writing:
5
5
  #
6
6
  # cookies["user_name"] = "david" # => Will set a simple session cookie
7
7
  # cookies["login"] = { :value => "XJ-122", :expires => Time.now + 360} # => Will set a cookie that expires in 1 hour
@@ -26,6 +26,9 @@ module ActionController #:nodoc:
26
26
  # # model :post (already required)
27
27
  # # helper :post (already required)
28
28
  # end
29
+ #
30
+ # Also note, that if the models follow the pattern of just 1 class per file in the form of MyClass => my_class.rb, then these
31
+ # classes doesn't have to be required as Active Support will auto-require them.
29
32
  module ClassMethods
30
33
  # Specifies a variable number of models that this controller depends on. Models are normally Active Record classes or a similar
31
34
  # backend for modelling entity classes.
@@ -70,6 +73,9 @@ module ActionController #:nodoc:
70
73
  require_dependency(dependency.to_s)
71
74
  rescue LoadError
72
75
  raise LoadError, "Missing #{layer} #{dependency}.rb"
76
+ rescue Object => exception
77
+ exception.blame_file! "=> #{layer} #{dependency}.rb"
78
+ raise
73
79
  end
74
80
  end
75
81
  end
@@ -78,7 +84,6 @@ module ActionController #:nodoc:
78
84
  inherited_without_model(child)
79
85
  return if child.controller_name == "application" # otherwise the ApplicationController in Rails will include itself
80
86
  begin
81
- Object.const_get(child.controller_name.singularize.classify)
82
87
  child.model(child.controller_name.singularize)
83
88
  rescue NameError, LoadError
84
89
  # No neither singular or plural model available for this controller
@@ -100,7 +100,7 @@ module ActionController #:nodoc:
100
100
  # == Around filters
101
101
  #
102
102
  # In addition to the individual before and after filters, it's also possible to specify that a single object should handle
103
- # both the before and after call. That's especially usefuly when you need to keep state active between the before and after,
103
+ # both the before and after call. That's especially useful when you need to keep state active between the before and after,
104
104
  # such as the example of a benchmark filter below:
105
105
  #
106
106
  # class WeblogController < ActionController::Base
@@ -1,13 +1,13 @@
1
1
  module ActionController #:nodoc:
2
2
  # The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed
3
3
  # to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create action
4
- # that sets <tt>flash["notice"] = "Succesfully created"</tt> before redirecting to a display action that can then expose
4
+ # that sets <tt>flash["notice"] = "Successfully created"</tt> before redirecting to a display action that can then expose
5
5
  # the flash to its template. Actually, that exposure is automatically done. Example:
6
6
  #
7
7
  # class WeblogController < ActionController::Base
8
8
  # def create
9
9
  # # save post
10
- # flash["notice"] = "Succesfully created post"
10
+ # flash["notice"] = "Successfully created post"
11
11
  # redirect_to :action => "display", :params => { "id" => post.id }
12
12
  # end
13
13
  #
@@ -62,4 +62,4 @@ module ActionController #:nodoc:
62
62
  end
63
63
  end
64
64
  end
65
- end
65
+ end
@@ -35,8 +35,7 @@ module ActionController #:nodoc:
35
35
  template_class.class_eval "include #{helper_module}"
36
36
  end
37
37
 
38
- # Declare a helper. If you use this method in your controller, you don't
39
- # have to do the +self.append_features+ incantation in your helper class.
38
+ # Declare a helper:
40
39
  # helper :foo
41
40
  # requires 'foo_helper' and includes FooHelper in the template class.
42
41
  # helper FooHelper
@@ -48,25 +47,22 @@ module ActionController #:nodoc:
48
47
  def helper(*args, &block)
49
48
  args.flatten.each do |arg|
50
49
  case arg
51
- when Module
52
- add_template_helper(arg)
53
- when String, Symbol
54
- file_name = Inflector.underscore(arg.to_s.downcase) + '_helper'
55
- class_name = Inflector.camelize(file_name)
56
- begin
57
- require_dependency(file_name)
58
- rescue LoadError => load_error
59
- requiree = / -- (.*?)(\.rb)?$/.match(load_error).to_a[1]
60
- if requiree == file_name
61
- raise LoadError, "Missing helper file helpers/#{file_name}.rb"
62
- else
63
- raise LoadError, "Can't load file: #{requiree}"
50
+ when Module
51
+ add_template_helper(arg)
52
+ when String, Symbol
53
+ file_name = arg.to_s.underscore + '_helper'
54
+ class_name = file_name.camelize
55
+
56
+ begin
57
+ require_dependency(file_name)
58
+ rescue LoadError => load_error
59
+ requiree = / -- (.*?)(\.rb)?$/.match(load_error).to_a[1]
60
+ raise LoadError, requiree == file_name ? "Missing helper file helpers/#{file_name}.rb" : "Can't load file: #{requiree}"
64
61
  end
65
- end
66
- raise ArgumentError, "Missing #{class_name} module in helpers/#{file_name}.rb" unless Object.const_defined?(class_name)
67
- add_template_helper(Object.const_get(class_name))
68
- else
69
- raise ArgumentError, 'helper expects String, Symbol, or Module argument'
62
+
63
+ add_template_helper(class_name.constantize)
64
+ else
65
+ raise ArgumentError, 'helper expects String, Symbol, or Module argument'
70
66
  end
71
67
  end
72
68
 
@@ -95,7 +91,7 @@ module ActionController #:nodoc:
95
91
  def inherited(child)
96
92
  inherited_without_helper(child)
97
93
  begin
98
- child.helper(child.controller_name)
94
+ child.helper(child.controller_path)
99
95
  rescue ArgumentError, LoadError
100
96
  # No default helper available for this controller
101
97
  end
@@ -65,7 +65,7 @@ module ActionController #:nodoc:
65
65
  # <tt>app/views/layouts/weblog.rhtml</tt> or <tt>app/views/layouts/weblog.rxml</tt> exists then it will be automatically set as
66
66
  # the layout for your WeblogController. You can create a layout with the name <tt>application.rhtml</tt> or <tt>application.rxml</tt>
67
67
  # and this will be set as the default controller if there is no layout with the same name as the current controller and there is
68
- # no layout explicitly assigned with the +layout+ method. Setting a layout explicity will always override the automatic behaviour.
68
+ # no layout explicitly assigned with the +layout+ method. Setting a layout explicitly will always override the automatic behaviour.
69
69
  #
70
70
  # == Inheritance for layouts
71
71
  #
@@ -136,7 +136,7 @@ module ActionController #:nodoc:
136
136
  # This will assign "weblog_standard" as the WeblogController's layout except for the +rss+ action, which will not wrap a layout
137
137
  # around the rendered view.
138
138
  #
139
- # Both the +:only+ and +:except+ condition can accept an aribtrary number of method references, so +:except => [ :rss, :text_only ]+
139
+ # Both the +:only+ and +:except+ condition can accept an arbitrary number of method references, so +:except => [ :rss, :text_only ]+
140
140
  # is valid, as is # +:except => :rss+.
141
141
  #
142
142
  # == Using a different layout in the action render call
@@ -3,7 +3,8 @@ module ActionController
3
3
  class AbstractRequest
4
4
  # Returns both GET and POST parameters in a single hash.
5
5
  def parameters
6
- @parameters ||= request_parameters.update(query_parameters)
6
+ # puts "#{request_parameters.inspect} | #{query_parameters.inspect} | #{path_parameters.inspect}"
7
+ @parameters ||= request_parameters.merge(query_parameters).merge(path_parameters).with_indifferent_access
7
8
  end
8
9
 
9
10
  def method
@@ -42,7 +43,7 @@ module ActionController
42
43
 
43
44
  if env.include? 'HTTP_X_FORWARDED_FOR' then
44
45
  remote_ips = env['HTTP_X_FORWARDED_FOR'].split(',').reject do |ip|
45
- ip =~ /^unknown$|^(10|172\.16|192\.168)\./i
46
+ ip =~ /^unknown$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\./i
46
47
  end
47
48
 
48
49
  return remote_ips.first.strip unless remote_ips.empty?
@@ -65,7 +66,7 @@ module ActionController
65
66
  parts - parts.last(1 + tld_length)
66
67
  end
67
68
 
68
- # Recieve the raw post data.
69
+ # Receive the raw post data.
69
70
  # This is useful for services such as REST, XMLRPC and SOAP
70
71
  # which communicate over HTTP POST but don't use the traditional parameter format.
71
72
  def raw_post
@@ -73,11 +74,11 @@ module ActionController
73
74
  end
74
75
 
75
76
  def request_uri
76
- env['REQUEST_URI']
77
+ (%r{^\w+\://[^/]+(/.*|$)$} =~ env['REQUEST_URI']) ? $1 : env['REQUEST_URI'] # Remove domain, which webrick puts into the request_uri.
77
78
  end
78
79
 
79
80
  def protocol
80
- port == 443 ? 'https://' : 'http://'
81
+ env["HTTPS"] == "on" ? 'https://' : 'http://'
81
82
  end
82
83
 
83
84
  def ssl?
@@ -85,7 +86,7 @@ module ActionController
85
86
  end
86
87
 
87
88
  def path
88
- request_uri ? request_uri.split('?').first : ''
89
+ path = request_uri ? request_uri.split('?').first : ''
89
90
  end
90
91
 
91
92
  def port
@@ -100,7 +101,16 @@ module ActionController
100
101
  def host_with_port
101
102
  env['HTTP_HOST'] || host + port_string
102
103
  end
104
+
105
+ def path_parameters=(parameters)
106
+ @path_parameters = parameters
107
+ @parameters = nil
108
+ end
103
109
 
110
+ def path_parameters
111
+ @path_parameters ||= {}
112
+ end
113
+
104
114
  #--
105
115
  # Must be implemented in the concrete request
106
116
  #++
@@ -48,10 +48,14 @@ module ActionController #:nodoc:
48
48
 
49
49
  # Overwrite to implement public exception handling (for requests answering false to <tt>local_request?</tt>).
50
50
  def rescue_action_in_public(exception) #:doc:
51
- render_text "<html><body><h1>Application error (Rails)</h1></body></html>"
51
+ case exception
52
+ when RoutingError, UnknownAction then
53
+ render_text(IO.read(File.join(RAILS_ROOT, 'public', '404.html')), "404 Not Found")
54
+ else render_text "<html><body><h1>Application error (Rails)</h1></body></html>"
55
+ end
52
56
  end
53
57
 
54
- # Overwrite to expand the meaning of a local request in order to show local rescues on other occurances than
58
+ # Overwrite to expand the meaning of a local request in order to show local rescues on other occurences than
55
59
  # the remote IP being 127.0.0.1. For example, this could include the IP of the developer machine when debugging
56
60
  # remotely.
57
61
  def local_request? #:doc:
@@ -66,7 +70,7 @@ module ActionController #:nodoc:
66
70
  @contents = @template.render_file(template_path_for_local_rescue(exception), false)
67
71
 
68
72
  @headers["Content-Type"] = "text/html"
69
- render_file(rescues_path("layout"), "500 Internal Error")
73
+ render_file(rescues_path("layout"), response_code_for_rescue(exception))
70
74
  end
71
75
 
72
76
  private
@@ -110,6 +114,7 @@ module ActionController #:nodoc:
110
114
  rescues_path(
111
115
  case exception
112
116
  when MissingTemplate then "missing_template"
117
+ when RoutingError then "routing_error"
113
118
  when UnknownAction then "unknown_action"
114
119
  when ActionView::TemplateError then "template_error"
115
120
  else "diagnostics"
@@ -117,6 +122,13 @@ module ActionController #:nodoc:
117
122
  )
118
123
  end
119
124
 
125
+ def response_code_for_rescue(exception)
126
+ case exception
127
+ when UnknownAction, RoutingError then "404 Page Not Found"
128
+ else "500 Internal Error"
129
+ end
130
+ end
131
+
120
132
  def clean_backtrace(exception)
121
133
  exception.backtrace.collect { |line| Object.const_defined?(:RAILS_ROOT) ? line.gsub(RAILS_ROOT, "") : line }
122
134
  end
@@ -0,0 +1,304 @@
1
+ module ActionController
2
+ # See http://manuals.rubyonrails.com/read/chapter/65
3
+ module Routing
4
+ ROUTE_FILE = defined?(RAILS_ROOT) ? File.expand_path(File.join(RAILS_ROOT, 'config', 'routes')) : nil
5
+
6
+ class Route #:nodoc:
7
+ attr_reader :defaults # The defaults hash
8
+
9
+ def initialize(path, hash={})
10
+ raise ArgumentError, "Second argument must be a hash!" unless hash.kind_of?(Hash)
11
+ @defaults = hash[:defaults].kind_of?(Hash) ? hash.delete(:defaults) : {}
12
+ @requirements = hash[:requirements].kind_of?(Hash) ? hash.delete(:requirements) : {}
13
+ self.items = path
14
+ hash.each do |k, v|
15
+ raise TypeError, "Hash keys must be symbols!" unless k.kind_of? Symbol
16
+ if v.kind_of? Regexp
17
+ raise ArgumentError, "Regexp requirement on #{k}, but #{k} is not in this route's path!" unless @items.include? k
18
+ @requirements[k] = v
19
+ else
20
+ (@items.include?(k) ? @defaults : @requirements)[k] = v
21
+ end
22
+ end
23
+
24
+ @defaults.each do |k, v|
25
+ raise ArgumentError, "A default has been specified for #{k}, but #{k} is not in the path!" unless @items.include? k
26
+ @defaults[k] = v.to_s unless v.kind_of?(String) || v.nil?
27
+ end
28
+ @requirements.each {|k, v| raise ArgumentError, "A Regexp requirement has been specified for #{k}, but #{k} is not in the path!" if v.kind_of?(Regexp) && ! @items.include?(k)}
29
+
30
+ # Add in defaults for :action and :id.
31
+ [[:action, 'index'], [:id, nil]].each do |name, default|
32
+ @defaults[name] = default if @items.include?(name) && ! (@requirements.key?(name) || @defaults.key?(name))
33
+ end
34
+ end
35
+
36
+ # Generate a URL given the provided options.
37
+ # All values in options should be symbols.
38
+ # Returns the path and the unused names in a 2 element array.
39
+ # If generation fails, [nil, nil] is returned
40
+ # Generation can fail because of a missing value, or because an equality check fails.
41
+ #
42
+ # Generate urls will be as short as possible. If the last component of a url is equal to the default value,
43
+ # then that component is removed. This is applied as many times as possible. So, your index controller's
44
+ # index action will generate []
45
+ def generate(options, defaults={})
46
+ non_matching = @requirements.keys.select {|name| ! passes_requirements?(name, options[name] || defaults[name])}
47
+ non_matching.collect! {|name| requirements_for(name)}
48
+ return nil, "Mismatching option#{'s' if non_matching.length > 1}:\n #{non_matching.join '\n '}" unless non_matching.empty?
49
+
50
+ used_names = @requirements.inject({}) {|hash, (k, v)| hash[k] = true; hash} # Mark requirements as used so they don't get put in the query params
51
+ components = @items.collect do |item|
52
+ if item.kind_of? Symbol
53
+ used_names[item] = true
54
+ value = options[item] || defaults[item] || @defaults[item]
55
+ return nil, requirements_for(item) unless passes_requirements?(item, value)
56
+ defaults = {} unless defaults == {} || value == defaults[item] # Stop using defaults if this component isn't the same as the default.
57
+ (value.nil? || item == :controller) ? value : CGI.escape(value.to_s)
58
+ else item
59
+ end
60
+ end
61
+
62
+ @items.reverse_each do |item| # Remove default components from the end of the generated url.
63
+ break unless item.kind_of?(Symbol) && @defaults[item] == components.last
64
+ components.pop
65
+ end
66
+
67
+ # If we have any nil components then we can't proceed.
68
+ # This might need to be changed. In some cases we may be able to return all componets after nil as extras.
69
+ missing = []; components.each_with_index {|c, i| missing << @items[i] if c.nil?}
70
+ return nil, "No values provided for component#{'s' if missing.length > 1} #{missing.join ', '} but values are required due to use of later components" unless missing.empty? # how wide is your screen?
71
+
72
+ unused = (options.keys - used_names.keys).inject({}) do |unused, key|
73
+ unused[key] = options[key] if options[key] != @defaults[key]
74
+ unused
75
+ end
76
+
77
+ components.collect! {|c| c.to_s}
78
+ return components, unused
79
+ end
80
+
81
+ # Recognize the provided path, returning a hash of recognized values, or [nil, reason] if the path isn't recognized.
82
+ # The path should be a list of component strings.
83
+ # Options is a hash of the ?k=v pairs
84
+ def recognize(components, options={})
85
+ options = options.clone
86
+ components = components.clone
87
+ controller_class = nil
88
+
89
+ @items.each do |item|
90
+ if item == :controller # Special case for controller
91
+ if components.empty? && @defaults[:controller]
92
+ controller_class, leftover = eat_path_to_controller(@defaults[:controller].split('/'))
93
+ raise RoutingError, "Default controller does not exist: #{@defaults[:controller]}" if controller_class.nil? || leftover.empty? == false
94
+ else
95
+ controller_class, remaining_components = eat_path_to_controller(components)
96
+ return nil, "No controller found at subpath #{components.join('/')}" if controller_class.nil?
97
+ components = remaining_components
98
+ end
99
+ options[:controller] = controller_class.controller_path
100
+ return nil, requirements_for(:controller) unless passes_requirements?(:controller, options[:controller])
101
+ elsif item.kind_of? Symbol
102
+ value = components.shift || @defaults[item]
103
+ return nil, requirements_for(item) unless passes_requirements?(item, value)
104
+ options[item] = value.nil? ? value : CGI.unescape(value)
105
+ else
106
+ return nil, "No value available for component #{item.inspect}" if components.empty?
107
+ component = components.shift
108
+ return nil, "Value for component #{item.inspect} doesn't match #{component}" if component != item
109
+ end
110
+ end
111
+
112
+ if controller_class.nil? && @requirements[:controller] # Load a default controller
113
+ controller_class, extras = eat_path_to_controller(@requirements[:controller].split('/'))
114
+ raise RoutingError, "Illegal controller path for route default: #{@requirements[:controller]}" unless controller_class && extras.empty?
115
+ options[:controller] = controller_class.controller_path
116
+ end
117
+ @requirements.each {|k,v| options[k] ||= v unless v.kind_of?(Regexp)}
118
+
119
+ return nil, "Route recognition didn't find a controller class!" unless controller_class
120
+ return nil, "Unused components were left: #{components.join '/'}" unless components.empty?
121
+ options.delete_if {|k, v| v.nil?} # Remove nil values.
122
+ return controller_class, options
123
+ end
124
+
125
+ def inspect
126
+ when_str = @requirements.empty? ? "" : " when #{@requirements.inspect}"
127
+ default_str = @defaults.empty? ? "" : " || #{@defaults.inspect}"
128
+ "<#{self.class.to_s} #{@items.collect{|c| c.kind_of?(String) ? c : c.inspect}.join('/').inspect}#{default_str}#{when_str}>"
129
+ end
130
+
131
+ protected
132
+ # Find the controller given a list of path components.
133
+ # Return the controller class and the unused path components.
134
+ def eat_path_to_controller(path)
135
+ path.inject([Controllers, 1]) do |(mod, length), name|
136
+ name = name.camelize
137
+ controller_name = name + "Controller"
138
+ return mod.const_get(controller_name), path[length..-1] if mod.const_available? controller_name
139
+ return nil, nil unless mod.const_available? name
140
+ [mod.const_get(name), length + 1]
141
+ end
142
+ return nil, nil # Path ended, but no controller found.
143
+ end
144
+
145
+ def items=(path)
146
+ items = path.split('/').collect {|c| (/^:(\w+)$/ =~ c) ? $1.intern : c} if path.kind_of?(String) # split and convert ':xyz' to symbols
147
+ items.shift if items.first == ""
148
+ items.pop if items.last == ""
149
+ @items = items
150
+
151
+ # Verify uniqueness of each component.
152
+ @items.inject({}) do |seen, item|
153
+ if item.kind_of? Symbol
154
+ raise ArgumentError, "Illegal route path -- duplicate item #{item}\n #{path.inspect}" if seen.key? item
155
+ seen[item] = true
156
+ end
157
+ seen
158
+ end
159
+ end
160
+
161
+ # Verify that the given value passes this route's requirements
162
+ def passes_requirements?(name, value)
163
+ return @defaults.key?(name) && @defaults[name].nil? if value.nil? # Make sure it's there if it should be
164
+
165
+ case @requirements[name]
166
+ when nil then true
167
+ when Regexp then
168
+ value = value.to_s
169
+ match = @requirements[name].match(value)
170
+ match && match[0].length == value.length
171
+ else
172
+ @requirements[name] == value.to_s
173
+ end
174
+ end
175
+ def requirements_for(name)
176
+ presence = (@defaults.key?(name) && @defaults[name].nil?)
177
+ requirement = case @requirements[name]
178
+ when nil then nil
179
+ when Regexp then "match #{@requirements[name].inspect}"
180
+ else "be equal to #{@requirements[name].inspect}"
181
+ end
182
+ if presence && requirement then "#{name} must be present and #{requirement}"
183
+ elsif presence || requirement then "#{name} must #{requirement || 'be present'}"
184
+ else "#{name} has no requirements"
185
+ end
186
+ end
187
+ end
188
+
189
+ class RouteSet#:nodoc:
190
+ def initialize
191
+ @routes = []
192
+ end
193
+
194
+ def add_route(route)
195
+ raise TypeError, "#{route.inspect} is not a Route instance!" unless route.kind_of?(Route)
196
+ @routes << route
197
+ end
198
+ def empty?
199
+ @routes.empty?
200
+ end
201
+ def each
202
+ @routes.each {|route| yield route}
203
+ end
204
+
205
+ # Generate a path for the provided options
206
+ # Returns the path as an array of components and a hash of unused names
207
+ # Raises RoutingError if not route can handle the provided components.
208
+ #
209
+ # Note that we don't return the first generated path. We do this so that when a route
210
+ # generates a path from a subset of the available options we can keep looking for a
211
+ # route which can generate a path that uses more options.
212
+ # Note that we *do* return immediately if
213
+ def generate(options, request)
214
+ raise RoutingError, "There are no routes defined!" if @routes.empty?
215
+
216
+ options = options.symbolize_keys
217
+ defaults = request.path_parameters.symbolize_keys
218
+ if options.empty? then options = defaults.clone # Get back the current url if no options was passed
219
+ else expand_controller_path!(options, defaults) # Expand the supplied controller path.
220
+ end
221
+ defaults.delete_if {|k, v| options.key?(k) && options[k].nil?} # Remove defaults that have been manually cleared using :name => nil
222
+
223
+ failures = []
224
+ selected = nil
225
+ self.each do |route|
226
+ path, unused = route.generate(options, defaults)
227
+ if path.nil?
228
+ failures << [route, unused] if ActionController::Base.debug_routes
229
+ else
230
+ return path, unused if unused.empty? # Found a perfect route -- we're finished.
231
+ if selected.nil? || unused.length < selected.last.length
232
+ failures << [selected.first, "A better url than #{selected[1]} was found."] if selected
233
+ selected = [route, path, unused]
234
+ end
235
+ end
236
+ end
237
+
238
+ return selected[1..-1] unless selected.nil?
239
+ raise RoutingError.new("Generation failure: No route for url_options #{options.inspect}, defaults: #{defaults.inspect}", failures)
240
+ end
241
+
242
+ # Recognize the provided path.
243
+ # Raise RoutingError if the path can't be recognized.
244
+ def recognize!(request)
245
+ path = ((%r{^/?(.*)/?$} =~ request.path) ? $1 : request.path).split('/')
246
+ raise RoutingError, "There are no routes defined!" if @routes.empty?
247
+
248
+ failures = []
249
+ self.each do |route|
250
+ controller, options = route.recognize(path)
251
+ if controller.nil?
252
+ failures << [route, options] if ActionController::Base.debug_routes
253
+ else
254
+ request.path_parameters = options
255
+ return controller
256
+ end
257
+ end
258
+
259
+ raise RoutingError.new("No route for path: #{path.join('/').inspect}", failures)
260
+ end
261
+
262
+ def expand_controller_path!(options, defaults)
263
+ if options[:controller]
264
+ if /^\// =~ options[:controller]
265
+ options[:controller] = options[:controller][1..-1]
266
+ defaults.clear # Sending to absolute controller implies fresh defaults
267
+ else
268
+ relative_to = defaults[:controller] ? defaults[:controller].split('/')[0..-2].join('/') : ''
269
+ options[:controller] = relative_to.empty? ? options[:controller] : "#{relative_to}/#{options[:controller]}"
270
+ defaults.delete(:action) if options.key?(:controller)
271
+ end
272
+ else
273
+ options[:controller] = defaults[:controller]
274
+ end
275
+ end
276
+
277
+ def route(*args)
278
+ add_route(Route.new(*args))
279
+ end
280
+ alias :connect :route
281
+
282
+ def reload
283
+ begin
284
+ require_dependency(ROUTE_FILE) if ROUTE_FILE
285
+ rescue LoadError, ScriptError => e
286
+ raise RoutingError, "Cannot load config/routes.rb:\n #{e.message}"
287
+ ensure # Ensure that there is at least one route:
288
+ connect(':controller/:action/:id', :action => 'index', :id => nil) if @routes.empty?
289
+ end
290
+ end
291
+
292
+ def draw
293
+ @routes.clear
294
+ yield self
295
+ end
296
+ end
297
+
298
+ def self.draw(*args, &block) #:nodoc:
299
+ Routes.draw(*args) {|*args| block.call(*args)}
300
+ end
301
+
302
+ Routes = RouteSet.new
303
+ end
304
+ end