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.
- data/CHANGELOG +66 -0
- data/README +94 -64
- data/install.rb +1 -20
- data/lib/action_controller.rb +15 -7
- data/lib/action_controller/assertions/action_pack_assertions.rb +56 -3
- data/lib/action_controller/base.rb +137 -64
- data/lib/action_controller/caching.rb +11 -11
- data/lib/action_controller/cgi_ext/cgi_ext.rb +2 -2
- data/lib/action_controller/cgi_ext/raw_post_data_fix.rb +4 -4
- data/lib/action_controller/cgi_process.rb +9 -1
- data/lib/action_controller/components.rb +73 -0
- data/lib/action_controller/cookies.rb +1 -1
- data/lib/action_controller/dependencies.rb +6 -1
- data/lib/action_controller/filters.rb +1 -1
- data/lib/action_controller/flash.rb +3 -3
- data/lib/action_controller/helpers.rb +17 -21
- data/lib/action_controller/layout.rb +2 -2
- data/lib/action_controller/request.rb +16 -6
- data/lib/action_controller/rescue.rb +15 -3
- data/lib/action_controller/routing.rb +304 -0
- data/lib/action_controller/scaffolding.rb +10 -12
- data/lib/action_controller/session/active_record_store.rb +4 -7
- data/lib/action_controller/session/mem_cache_store.rb +2 -2
- data/lib/action_controller/templates/rescues/_request_and_response.rhtml +9 -1
- data/lib/action_controller/templates/rescues/diagnostics.rhtml +1 -1
- data/lib/action_controller/templates/rescues/routing_error.rhtml +8 -0
- data/lib/action_controller/test_process.rb +29 -7
- data/lib/action_controller/url_rewriter.rb +28 -112
- data/lib/action_view.rb +1 -1
- data/lib/action_view/base.rb +44 -17
- data/lib/action_view/helpers/active_record_helper.rb +1 -1
- data/lib/action_view/helpers/asset_tag_helper.rb +59 -0
- data/lib/action_view/helpers/date_helper.rb +24 -13
- data/lib/action_view/helpers/form_helper.rb +6 -1
- data/lib/action_view/helpers/form_options_helper.rb +87 -9
- data/lib/action_view/helpers/form_tag_helper.rb +80 -0
- data/lib/action_view/helpers/tag_helper.rb +0 -23
- data/lib/action_view/helpers/text_helper.rb +26 -1
- data/lib/action_view/helpers/url_helper.rb +29 -35
- data/lib/action_view/partials.rb +2 -2
- data/lib/action_view/vendor/builder/xmlbase.rb +3 -3
- data/rakefile +5 -2
- data/test/abstract_unit.rb +3 -1
- data/test/controller/action_pack_assertions_test.rb +29 -1
- data/test/controller/active_record_assertions_test.rb +109 -101
- data/test/controller/base_tests.rb +72 -0
- data/test/controller/components_test.rb +74 -0
- data/test/controller/cookie_test.rb +0 -9
- data/test/controller/custom_handler_test.rb +33 -0
- data/test/controller/filters_test.rb +36 -0
- data/test/controller/helper_test.rb +27 -10
- data/test/controller/redirect_test.rb +23 -31
- data/test/controller/render_test.rb +81 -66
- data/test/controller/request_test.rb +22 -0
- data/test/controller/routing_tests.rb +490 -0
- data/test/controller/{url_test.rb → url_obsolete.rb} +24 -14
- data/test/controller/url_obsolete.rb.rej +747 -0
- data/test/fixtures/fun/games/hello_world.rhtml +1 -0
- data/test/fixtures/helpers/fun/games_helper.rb +3 -0
- data/test/template/asset_tag_helper_test.rb +45 -0
- data/test/template/form_options_helper_test.rb +161 -1
- data/test/template/form_tag_helper_test.rb +22 -0
- data/test/template/text_helper_test.rb +7 -0
- data/test/template/url_helper_test.rb +5 -2
- data/test/template/url_helper_test.rb.rej +105 -0
- metadata +32 -27
- data/lib/action_controller/support/binding_of_caller.rb +0 -83
- data/lib/action_controller/support/breakpoint.rb +0 -518
- data/lib/action_controller/support/class_attribute_accessors.rb +0 -57
- data/lib/action_controller/support/class_inheritable_attributes.rb +0 -117
- data/lib/action_controller/support/clean_logger.rb +0 -10
- data/lib/action_controller/support/core_ext.rb +0 -1
- data/lib/action_controller/support/core_ext/hash.rb +0 -5
- data/lib/action_controller/support/core_ext/hash/keys.rb +0 -35
- data/lib/action_controller/support/core_ext/numeric.rb +0 -7
- data/lib/action_controller/support/core_ext/numeric/bytes.rb +0 -33
- data/lib/action_controller/support/core_ext/numeric/time.rb +0 -59
- data/lib/action_controller/support/core_ext/object_and_class.rb +0 -24
- data/lib/action_controller/support/core_ext/string.rb +0 -5
- data/lib/action_controller/support/core_ext/string/inflections.rb +0 -45
- data/lib/action_controller/support/dependencies.rb +0 -63
- data/lib/action_controller/support/inflector.rb +0 -84
- data/lib/action_controller/support/misc.rb +0 -8
- 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
|
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
|
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"] = "
|
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"] = "
|
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
|
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
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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.
|
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
|
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
|
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
|
-
|
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\.
|
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
|
-
#
|
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
|
-
|
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
|
-
|
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
|
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"),
|
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
|