actionpack 2.0.5 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +149 -7
- data/MIT-LICENSE +1 -1
- data/README +1 -1
- data/Rakefile +5 -6
- data/lib/action_controller.rb +2 -2
- data/lib/action_controller/assertions/model_assertions.rb +2 -1
- data/lib/action_controller/assertions/response_assertions.rb +4 -2
- data/lib/action_controller/assertions/routing_assertions.rb +3 -3
- data/lib/action_controller/assertions/selector_assertions.rb +30 -27
- data/lib/action_controller/assertions/tag_assertions.rb +3 -3
- data/lib/action_controller/base.rb +103 -129
- data/lib/action_controller/benchmarking.rb +3 -3
- data/lib/action_controller/caching.rb +41 -652
- data/lib/action_controller/caching/actions.rb +144 -0
- data/lib/action_controller/caching/fragments.rb +138 -0
- data/lib/action_controller/caching/pages.rb +154 -0
- data/lib/action_controller/caching/sql_cache.rb +18 -0
- data/lib/action_controller/caching/sweeping.rb +97 -0
- data/lib/action_controller/cgi_ext/cookie.rb +27 -23
- data/lib/action_controller/cgi_ext/stdinput.rb +1 -0
- data/lib/action_controller/cgi_process.rb +6 -4
- data/lib/action_controller/components.rb +7 -6
- data/lib/action_controller/cookies.rb +31 -19
- data/lib/action_controller/dispatcher.rb +51 -84
- data/lib/action_controller/filters.rb +295 -421
- data/lib/action_controller/flash.rb +1 -6
- data/lib/action_controller/headers.rb +31 -0
- data/lib/action_controller/helpers.rb +26 -9
- data/lib/action_controller/http_authentication.rb +1 -1
- data/lib/action_controller/integration.rb +65 -13
- data/lib/action_controller/layout.rb +24 -39
- data/lib/action_controller/mime_responds.rb +7 -3
- data/lib/action_controller/mime_type.rb +25 -9
- data/lib/action_controller/mime_types.rb +1 -1
- data/lib/action_controller/polymorphic_routes.rb +32 -17
- data/lib/action_controller/record_identifier.rb +10 -4
- data/lib/action_controller/request.rb +46 -30
- data/lib/action_controller/request_forgery_protection.rb +10 -9
- data/lib/action_controller/request_profiler.rb +29 -8
- data/lib/action_controller/rescue.rb +24 -24
- data/lib/action_controller/resources.rb +66 -23
- data/lib/action_controller/response.rb +2 -2
- data/lib/action_controller/routing.rb +113 -1229
- data/lib/action_controller/routing/builder.rb +204 -0
- data/lib/action_controller/{routing_optimisation.rb → routing/optimisations.rb} +13 -12
- data/lib/action_controller/routing/recognition_optimisation.rb +158 -0
- data/lib/action_controller/routing/route.rb +240 -0
- data/lib/action_controller/routing/route_set.rb +435 -0
- data/lib/action_controller/routing/routing_ext.rb +46 -0
- data/lib/action_controller/routing/segments.rb +283 -0
- data/lib/action_controller/session/active_record_store.rb +13 -8
- data/lib/action_controller/session/cookie_store.rb +20 -17
- data/lib/action_controller/session_management.rb +10 -3
- data/lib/action_controller/streaming.rb +45 -31
- data/lib/action_controller/test_case.rb +33 -23
- data/lib/action_controller/test_process.rb +39 -35
- data/lib/action_controller/url_rewriter.rb +18 -12
- data/lib/action_controller/vendor/html-scanner/html/tokenizer.rb +1 -1
- data/lib/action_pack.rb +1 -1
- data/lib/action_pack/version.rb +2 -2
- data/lib/action_view.rb +11 -3
- data/lib/action_view/base.rb +73 -390
- data/lib/action_view/helpers/active_record_helper.rb +83 -62
- data/lib/action_view/helpers/asset_tag_helper.rb +101 -44
- data/lib/action_view/helpers/atom_feed_helper.rb +35 -7
- data/lib/action_view/helpers/benchmark_helper.rb +5 -3
- data/lib/action_view/helpers/cache_helper.rb +3 -2
- data/lib/action_view/helpers/capture_helper.rb +1 -2
- data/lib/action_view/helpers/date_helper.rb +104 -82
- data/lib/action_view/helpers/form_helper.rb +148 -75
- data/lib/action_view/helpers/form_options_helper.rb +44 -23
- data/lib/action_view/helpers/form_tag_helper.rb +22 -13
- data/lib/action_view/helpers/javascripts/controls.js +1 -1
- data/lib/action_view/helpers/javascripts/dragdrop.js +1 -1
- data/lib/action_view/helpers/javascripts/effects.js +1 -1
- data/lib/action_view/helpers/number_helper.rb +10 -3
- data/lib/action_view/helpers/prototype_helper.rb +61 -29
- data/lib/action_view/helpers/record_tag_helper.rb +3 -3
- data/lib/action_view/helpers/sanitize_helper.rb +23 -17
- data/lib/action_view/helpers/scriptaculous_helper.rb +86 -60
- data/lib/action_view/helpers/text_helper.rb +153 -125
- data/lib/action_view/helpers/url_helper.rb +83 -28
- data/lib/action_view/inline_template.rb +20 -0
- data/lib/action_view/partial_template.rb +70 -0
- data/lib/action_view/partials.rb +31 -73
- data/lib/action_view/template.rb +127 -0
- data/lib/action_view/template_error.rb +8 -7
- data/lib/action_view/template_finder.rb +177 -0
- data/lib/action_view/template_handler.rb +18 -1
- data/lib/action_view/template_handlers/builder.rb +10 -2
- data/lib/action_view/template_handlers/compilable.rb +128 -0
- data/lib/action_view/template_handlers/erb.rb +37 -2
- data/lib/action_view/template_handlers/rjs.rb +14 -1
- data/lib/action_view/test_case.rb +58 -0
- data/test/abstract_unit.rb +1 -1
- data/test/active_record_unit.rb +3 -6
- data/test/activerecord/active_record_store_test.rb +1 -2
- data/test/activerecord/render_partial_with_record_identification_test.rb +158 -41
- data/test/adv_attr_test.rb +20 -0
- data/test/controller/action_pack_assertions_test.rb +16 -19
- data/test/controller/addresses_render_test.rb +1 -1
- data/test/controller/assert_select_test.rb +13 -6
- data/test/controller/base_test.rb +48 -2
- data/test/controller/benchmark_test.rb +1 -2
- data/test/controller/caching_test.rb +282 -21
- data/test/controller/capture_test.rb +1 -1
- data/test/controller/cgi_test.rb +1 -1
- data/test/controller/components_test.rb +1 -1
- data/test/controller/content_type_test.rb +2 -2
- data/test/controller/cookie_test.rb +13 -2
- data/test/controller/custom_handler_test.rb +14 -10
- data/test/controller/deprecation/deprecated_base_methods_test.rb +1 -1
- data/test/controller/dispatcher_test.rb +31 -49
- data/test/controller/fake_controllers.rb +17 -0
- data/test/controller/fake_models.rb +6 -0
- data/test/controller/filter_params_test.rb +14 -8
- data/test/controller/filters_test.rb +44 -16
- data/test/controller/flash_test.rb +2 -2
- data/test/controller/header_test.rb +14 -0
- data/test/controller/helper_test.rb +19 -15
- data/test/controller/html-scanner/document_test.rb +1 -2
- data/test/controller/html-scanner/node_test.rb +1 -2
- data/test/controller/html-scanner/sanitizer_test.rb +8 -5
- data/test/controller/html-scanner/tag_node_test.rb +1 -2
- data/test/controller/html-scanner/text_node_test.rb +2 -3
- data/test/controller/html-scanner/tokenizer_test.rb +8 -2
- data/test/controller/http_authentication_test.rb +1 -1
- data/test/controller/integration_test.rb +14 -16
- data/test/controller/integration_upload_test.rb +43 -0
- data/test/controller/layout_test.rb +26 -6
- data/test/controller/mime_responds_test.rb +39 -7
- data/test/controller/mime_type_test.rb +29 -5
- data/test/controller/new_render_test.rb +105 -34
- data/test/controller/polymorphic_routes_test.rb +32 -20
- data/test/controller/record_identifier_test.rb +38 -2
- data/test/controller/redirect_test.rb +21 -1
- data/test/controller/render_test.rb +59 -15
- data/test/controller/request_forgery_protection_test.rb +92 -5
- data/test/controller/request_test.rb +64 -6
- data/test/controller/rescue_test.rb +22 -6
- data/test/controller/resources_test.rb +102 -14
- data/test/controller/routing_test.rb +231 -19
- data/test/controller/selector_test.rb +2 -2
- data/test/controller/send_file_test.rb +14 -3
- data/test/controller/session/cookie_store_test.rb +16 -4
- data/test/controller/session/mem_cache_store_test.rb +3 -4
- data/test/controller/session_fixation_test.rb +1 -1
- data/test/controller/session_management_test.rb +23 -1
- data/test/controller/test_test.rb +39 -18
- data/test/controller/url_rewriter_test.rb +35 -1
- data/test/controller/verification_test.rb +1 -1
- data/test/controller/view_paths_test.rb +15 -12
- data/test/controller/webservice_test.rb +48 -3
- data/test/fixtures/bad_customers/_bad_customer.html.erb +1 -0
- data/test/fixtures/company.rb +1 -0
- data/test/fixtures/customers/_customer.html.erb +1 -0
- data/test/fixtures/db_definitions/sqlite.sql +6 -0
- data/test/fixtures/functional_caching/_partial.erb +3 -0
- data/test/fixtures/functional_caching/fragment_cached.html.erb +2 -0
- data/test/fixtures/functional_caching/html_fragment_cached_with_partial.html.erb +1 -0
- data/test/fixtures/functional_caching/js_fragment_cached_with_partial.js.rjs +1 -0
- data/test/fixtures/good_customers/_good_customer.html.erb +1 -0
- data/test/fixtures/mascot.rb +3 -0
- data/test/fixtures/mascots.yml +4 -0
- data/test/fixtures/mascots/_mascot.html.erb +1 -0
- data/test/fixtures/multipart/boundary_problem_file +10 -0
- data/test/fixtures/public/javascripts/application.js +1 -0
- data/test/fixtures/public/javascripts/controls.js +1 -0
- data/test/fixtures/public/javascripts/dragdrop.js +1 -0
- data/test/fixtures/public/javascripts/effects.js +1 -0
- data/test/fixtures/public/javascripts/prototype.js +1 -0
- data/test/fixtures/public/javascripts/version.1.0.js +1 -0
- data/test/fixtures/public/stylesheets/version.1.0.css +1 -0
- data/test/fixtures/reply.rb +1 -0
- data/test/fixtures/shared.html.erb +1 -0
- data/test/fixtures/symlink_parent/symlinked_layout.erb +5 -0
- data/test/fixtures/test/_customer_counter.erb +1 -0
- data/test/fixtures/test/_form.erb +1 -0
- data/test/fixtures/test/_labelling_form.erb +1 -0
- data/test/fixtures/test/_raise.html.erb +1 -0
- data/test/fixtures/test/greeting.js.rjs +1 -0
- data/test/fixtures/topics/_topic.html.erb +1 -0
- data/test/template/active_record_helper_test.rb +25 -8
- data/test/template/asset_tag_helper_test.rb +100 -17
- data/test/template/atom_feed_helper_test.rb +29 -1
- data/test/template/benchmark_helper_test.rb +10 -22
- data/test/template/date_helper_test.rb +455 -153
- data/test/template/erb_util_test.rb +10 -42
- data/test/template/form_helper_test.rb +192 -66
- data/test/template/form_options_helper_test.rb +19 -8
- data/test/template/form_tag_helper_test.rb +11 -8
- data/test/template/javascript_helper_test.rb +3 -9
- data/test/template/number_helper_test.rb +6 -3
- data/test/template/prototype_helper_test.rb +27 -40
- data/test/template/record_tag_helper_test.rb +54 -0
- data/test/template/sanitize_helper_test.rb +5 -6
- data/test/template/scriptaculous_helper_test.rb +7 -13
- data/test/template/tag_helper_test.rb +3 -6
- data/test/template/template_finder_test.rb +73 -0
- data/test/template/template_object_test.rb +95 -0
- data/test/template/test_test.rb +56 -0
- data/test/template/text_helper_test.rb +46 -33
- data/test/template/url_helper_test.rb +8 -10
- metadata +65 -12
- data/lib/action_view/compiled_templates.rb +0 -69
- data/test/action_view_test.rb +0 -44
- data/test/activerecord/fixtures_test.rb +0 -24
- data/test/controller/fragment_store_setting_test.rb +0 -47
- data/test/template/compiled_templates_test.rb +0 -197
- data/test/template/deprecate_ivars_test.rb +0 -51
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
module ActionController
|
|
2
|
+
module Routing
|
|
3
|
+
class RouteBuilder #:nodoc:
|
|
4
|
+
attr_accessor :separators, :optional_separators
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
self.separators = Routing::SEPARATORS
|
|
8
|
+
self.optional_separators = %w( / )
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def separator_pattern(inverted = false)
|
|
12
|
+
"[#{'^' if inverted}#{Regexp.escape(separators.join)}]"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def interval_regexp
|
|
16
|
+
Regexp.new "(.*?)(#{separators.source}|$)"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def multiline_regexp?(expression)
|
|
20
|
+
expression.options & Regexp::MULTILINE == Regexp::MULTILINE
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Accepts a "route path" (a string defining a route), and returns the array
|
|
24
|
+
# of segments that corresponds to it. Note that the segment array is only
|
|
25
|
+
# partially initialized--the defaults and requirements, for instance, need
|
|
26
|
+
# to be set separately, via the +assign_route_options+ method, and the
|
|
27
|
+
# <tt>optional?</tt> method for each segment will not be reliable until after
|
|
28
|
+
# +assign_route_options+ is called, as well.
|
|
29
|
+
def segments_for_route_path(path)
|
|
30
|
+
rest, segments = path, []
|
|
31
|
+
|
|
32
|
+
until rest.empty?
|
|
33
|
+
segment, rest = segment_for rest
|
|
34
|
+
segments << segment
|
|
35
|
+
end
|
|
36
|
+
segments
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# A factory method that returns a new segment instance appropriate for the
|
|
40
|
+
# format of the given string.
|
|
41
|
+
def segment_for(string)
|
|
42
|
+
segment = case string
|
|
43
|
+
when /\A:(\w+)/
|
|
44
|
+
key = $1.to_sym
|
|
45
|
+
case key
|
|
46
|
+
when :controller then ControllerSegment.new(key)
|
|
47
|
+
else DynamicSegment.new key
|
|
48
|
+
end
|
|
49
|
+
when /\A\*(\w+)/ then PathSegment.new($1.to_sym, :optional => true)
|
|
50
|
+
when /\A\?(.*?)\?/
|
|
51
|
+
returning segment = StaticSegment.new($1) do
|
|
52
|
+
segment.is_optional = true
|
|
53
|
+
end
|
|
54
|
+
when /\A(#{separator_pattern(:inverted)}+)/ then StaticSegment.new($1)
|
|
55
|
+
when Regexp.new(separator_pattern) then
|
|
56
|
+
returning segment = DividerSegment.new($&) do
|
|
57
|
+
segment.is_optional = (optional_separators.include? $&)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
[segment, $~.post_match]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Split the given hash of options into requirement and default hashes. The
|
|
64
|
+
# segments are passed alongside in order to distinguish between default values
|
|
65
|
+
# and requirements.
|
|
66
|
+
def divide_route_options(segments, options)
|
|
67
|
+
options = options.dup
|
|
68
|
+
|
|
69
|
+
if options[:namespace]
|
|
70
|
+
options[:controller] = "#{options[:path_prefix]}/#{options[:controller]}"
|
|
71
|
+
options.delete(:path_prefix)
|
|
72
|
+
options.delete(:name_prefix)
|
|
73
|
+
options.delete(:namespace)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
requirements = (options.delete(:requirements) || {}).dup
|
|
77
|
+
defaults = (options.delete(:defaults) || {}).dup
|
|
78
|
+
conditions = (options.delete(:conditions) || {}).dup
|
|
79
|
+
|
|
80
|
+
path_keys = segments.collect { |segment| segment.key if segment.respond_to?(:key) }.compact
|
|
81
|
+
options.each do |key, value|
|
|
82
|
+
hash = (path_keys.include?(key) && ! value.is_a?(Regexp)) ? defaults : requirements
|
|
83
|
+
hash[key] = value
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
[defaults, requirements, conditions]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Takes a hash of defaults and a hash of requirements, and assigns them to
|
|
90
|
+
# the segments. Any unused requirements (which do not correspond to a segment)
|
|
91
|
+
# are returned as a hash.
|
|
92
|
+
def assign_route_options(segments, defaults, requirements)
|
|
93
|
+
route_requirements = {} # Requirements that do not belong to a segment
|
|
94
|
+
|
|
95
|
+
segment_named = Proc.new do |key|
|
|
96
|
+
segments.detect { |segment| segment.key == key if segment.respond_to?(:key) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
requirements.each do |key, requirement|
|
|
100
|
+
segment = segment_named[key]
|
|
101
|
+
if segment
|
|
102
|
+
raise TypeError, "#{key}: requirements on a path segment must be regular expressions" unless requirement.is_a?(Regexp)
|
|
103
|
+
if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
|
|
104
|
+
raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
|
|
105
|
+
end
|
|
106
|
+
if multiline_regexp?(requirement)
|
|
107
|
+
raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}"
|
|
108
|
+
end
|
|
109
|
+
segment.regexp = requirement
|
|
110
|
+
else
|
|
111
|
+
route_requirements[key] = requirement
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
defaults.each do |key, default|
|
|
116
|
+
segment = segment_named[key]
|
|
117
|
+
raise ArgumentError, "#{key}: No matching segment exists; cannot assign default" unless segment
|
|
118
|
+
segment.is_optional = true
|
|
119
|
+
segment.default = default.to_param if default
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
assign_default_route_options(segments)
|
|
123
|
+
ensure_required_segments(segments)
|
|
124
|
+
route_requirements
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Assign default options, such as 'index' as a default for <tt>:action</tt>. This
|
|
128
|
+
# method must be run *after* user supplied requirements and defaults have
|
|
129
|
+
# been applied to the segments.
|
|
130
|
+
def assign_default_route_options(segments)
|
|
131
|
+
segments.each do |segment|
|
|
132
|
+
next unless segment.is_a? DynamicSegment
|
|
133
|
+
case segment.key
|
|
134
|
+
when :action
|
|
135
|
+
if segment.regexp.nil? || segment.regexp.match('index').to_s == 'index'
|
|
136
|
+
segment.default ||= 'index'
|
|
137
|
+
segment.is_optional = true
|
|
138
|
+
end
|
|
139
|
+
when :id
|
|
140
|
+
if segment.default.nil? && segment.regexp.nil? || segment.regexp =~ ''
|
|
141
|
+
segment.is_optional = true
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Makes sure that there are no optional segments that precede a required
|
|
148
|
+
# segment. If any are found that precede a required segment, they are
|
|
149
|
+
# made required.
|
|
150
|
+
def ensure_required_segments(segments)
|
|
151
|
+
allow_optional = true
|
|
152
|
+
segments.reverse_each do |segment|
|
|
153
|
+
allow_optional &&= segment.optional?
|
|
154
|
+
if !allow_optional && segment.optional?
|
|
155
|
+
unless segment.optionality_implied?
|
|
156
|
+
warn "Route segment \"#{segment.to_s}\" cannot be optional because it precedes a required segment. This segment will be required."
|
|
157
|
+
end
|
|
158
|
+
segment.is_optional = false
|
|
159
|
+
elsif allow_optional && segment.respond_to?(:default) && segment.default
|
|
160
|
+
# if a segment has a default, then it is optional
|
|
161
|
+
segment.is_optional = true
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Construct and return a route with the given path and options.
|
|
167
|
+
def build(path, options)
|
|
168
|
+
# Wrap the path with slashes
|
|
169
|
+
path = "/#{path}" unless path[0] == ?/
|
|
170
|
+
path = "#{path}/" unless path[-1] == ?/
|
|
171
|
+
|
|
172
|
+
path = "/#{options[:path_prefix].to_s.gsub(/^\//,'')}#{path}" if options[:path_prefix]
|
|
173
|
+
|
|
174
|
+
segments = segments_for_route_path(path)
|
|
175
|
+
defaults, requirements, conditions = divide_route_options(segments, options)
|
|
176
|
+
requirements = assign_route_options(segments, defaults, requirements)
|
|
177
|
+
|
|
178
|
+
route = Route.new
|
|
179
|
+
|
|
180
|
+
route.segments = segments
|
|
181
|
+
route.requirements = requirements
|
|
182
|
+
route.conditions = conditions
|
|
183
|
+
|
|
184
|
+
if !route.significant_keys.include?(:action) && !route.requirements[:action]
|
|
185
|
+
route.requirements[:action] = "index"
|
|
186
|
+
route.significant_keys << :action
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Routes cannot use the current string interpolation method
|
|
190
|
+
# if there are user-supplied <tt>:requirements</tt> as the interpolation
|
|
191
|
+
# code won't raise RoutingErrors when generating
|
|
192
|
+
if options.key?(:requirements) || route.requirements.keys.to_set != Routing::ALLOWED_REQUIREMENTS_FOR_OPTIMISATION
|
|
193
|
+
route.optimise = false
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
if !route.significant_keys.include?(:controller)
|
|
197
|
+
raise ArgumentError, "Illegal route: the :controller must be specified!"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
route
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
module ActionController
|
|
2
2
|
module Routing
|
|
3
3
|
# Much of the slow performance from routes comes from the
|
|
4
|
-
# complexity of expiry,
|
|
4
|
+
# complexity of expiry, <tt>:requirements</tt> matching, defaults providing
|
|
5
5
|
# and figuring out which url pattern to use. With named routes
|
|
6
6
|
# we can avoid the expense of finding the right route. So if
|
|
7
7
|
# they've provided the right number of arguments, and have no
|
|
8
|
-
#
|
|
8
|
+
# <tt>:requirements</tt>, we can just build up a string and return it.
|
|
9
9
|
#
|
|
10
10
|
# To support building optimisations for other common cases, the
|
|
11
11
|
# generation code is separated into several classes
|
|
@@ -41,28 +41,29 @@ module ActionController
|
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
# Temporarily disabled
|
|
44
|
+
# Temporarily disabled <tt>:url</tt> optimisation pending proper solution to
|
|
45
45
|
# Issues around request.host etc.
|
|
46
46
|
def applicable?
|
|
47
47
|
true
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
# Given a route
|
|
52
|
-
# map.person '/people/:id'
|
|
51
|
+
# Given a route
|
|
53
52
|
#
|
|
54
|
-
#
|
|
53
|
+
# map.person '/people/:id'
|
|
54
|
+
#
|
|
55
|
+
# If the user calls <tt>person_url(@person)</tt>, we can simply
|
|
55
56
|
# return a string like "/people/#{@person.to_param}"
|
|
56
|
-
# rather than triggering the expensive logic in url_for
|
|
57
|
+
# rather than triggering the expensive logic in +url_for+.
|
|
57
58
|
class PositionalArguments < Optimiser
|
|
58
59
|
def guard_condition
|
|
59
60
|
number_of_arguments = route.segment_keys.size
|
|
60
61
|
# if they're using foo_url(:id=>2) it's one
|
|
61
62
|
# argument, but we don't want to generate /foos/id2
|
|
62
63
|
if number_of_arguments == 1
|
|
63
|
-
"defined?(request) && request && args.size == 1 && !args.first.is_a?(Hash)"
|
|
64
|
+
"(!defined?(default_url_options) || default_url_options.blank?) && defined?(request) && request && args.size == 1 && !args.first.is_a?(Hash)"
|
|
64
65
|
else
|
|
65
|
-
"defined?(request) && request && args.size == #{number_of_arguments}"
|
|
66
|
+
"(!defined?(default_url_options) || default_url_options.blank?) && defined?(request) && request && args.size == #{number_of_arguments}"
|
|
66
67
|
end
|
|
67
68
|
end
|
|
68
69
|
|
|
@@ -77,7 +78,7 @@ module ActionController
|
|
|
77
78
|
|
|
78
79
|
elements << '#{request.relative_url_root if request.relative_url_root}'
|
|
79
80
|
|
|
80
|
-
# The last entry in route.segments appears to
|
|
81
|
+
# The last entry in <tt>route.segments</tt> appears to *always* be a
|
|
81
82
|
# 'divider segment' for '/' but we have assertions to ensure that
|
|
82
83
|
# we don't include the trailing slashes, so skip them.
|
|
83
84
|
(route.segments.size == 1 ? route.segments : route.segments[0..-2]).each do |segment|
|
|
@@ -97,7 +98,7 @@ module ActionController
|
|
|
97
98
|
# argument
|
|
98
99
|
class PositionalArgumentsWithAdditionalParams < PositionalArguments
|
|
99
100
|
def guard_condition
|
|
100
|
-
"defined?(request) && request && args.size == #{route.segment_keys.size + 1} && !args.last.has_key?(:anchor) && !args.last.has_key?(:port) && !args.last.has_key?(:host)"
|
|
101
|
+
"(!defined?(default_url_options) || default_url_options.blank?) && defined?(request) && request && args.size == #{route.segment_keys.size + 1} && !args.last.has_key?(:anchor) && !args.last.has_key?(:port) && !args.last.has_key?(:host)"
|
|
101
102
|
end
|
|
102
103
|
|
|
103
104
|
# This case uses almost the same code as positional arguments,
|
|
@@ -106,7 +107,7 @@ module ActionController
|
|
|
106
107
|
super.insert(-2, '?#{args.last.to_query}')
|
|
107
108
|
end
|
|
108
109
|
|
|
109
|
-
# To avoid generating http://localhost/?host=foo.example.com we
|
|
110
|
+
# To avoid generating "http://localhost/?host=foo.example.com" we
|
|
110
111
|
# can't use this optimisation on routes without any segments
|
|
111
112
|
def applicable?
|
|
112
113
|
super && route.segment_keys.size > 0
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
module ActionController
|
|
2
|
+
module Routing
|
|
3
|
+
# BEFORE: 0.191446860631307 ms/url
|
|
4
|
+
# AFTER: 0.029847304022858 ms/url
|
|
5
|
+
# Speed up: 6.4 times
|
|
6
|
+
#
|
|
7
|
+
# Route recognition is slow due to one-by-one iterating over
|
|
8
|
+
# a whole routeset (each map.resources generates at least 14 routes)
|
|
9
|
+
# and matching weird regexps on each step.
|
|
10
|
+
#
|
|
11
|
+
# We optimize this by skipping all URI segments that 100% sure can't
|
|
12
|
+
# be matched, moving deeper in a tree of routes (where node == segment)
|
|
13
|
+
# until first possible match is accured. In such case, we start walking
|
|
14
|
+
# a flat list of routes, matching them with accurate matcher.
|
|
15
|
+
# So, first step: search a segment tree for the first relevant index.
|
|
16
|
+
# Second step: iterate routes starting with that index.
|
|
17
|
+
#
|
|
18
|
+
# How tree is walked? We can do a recursive tests, but it's smarter:
|
|
19
|
+
# We just create a tree of if-s and elsif-s matching segments.
|
|
20
|
+
#
|
|
21
|
+
# We have segments of 3 flavors:
|
|
22
|
+
# 1) nil (no segment, route finished)
|
|
23
|
+
# 2) const-dot-dynamic (like "/posts.:xml", "/preview.:size.jpg")
|
|
24
|
+
# 3) const (like "/posts", "/comments")
|
|
25
|
+
# 4) dynamic ("/:id", "file.:size.:extension")
|
|
26
|
+
#
|
|
27
|
+
# We split incoming string into segments and iterate over them.
|
|
28
|
+
# When segment is nil, we drop immediately, on a current node index.
|
|
29
|
+
# When segment is equal to some const, we step into branch.
|
|
30
|
+
# If none constants matched, we step into 'dynamic' branch (it's a last).
|
|
31
|
+
# If we can't match anything, we drop to last index on a level.
|
|
32
|
+
#
|
|
33
|
+
# Note: we maintain the original routes order, so we finish building
|
|
34
|
+
# steps on a first dynamic segment.
|
|
35
|
+
#
|
|
36
|
+
#
|
|
37
|
+
# Example. Given the routes:
|
|
38
|
+
# 0 /posts/
|
|
39
|
+
# 1 /posts/:id
|
|
40
|
+
# 2 /posts/:id/comments
|
|
41
|
+
# 3 /posts/blah
|
|
42
|
+
# 4 /users/
|
|
43
|
+
# 5 /users/:id
|
|
44
|
+
# 6 /users/:id/profile
|
|
45
|
+
#
|
|
46
|
+
# request_uri = /users/123
|
|
47
|
+
#
|
|
48
|
+
# There will be only 4 iterations:
|
|
49
|
+
# 1) segm test for /posts prefix, skip all /posts/* routes
|
|
50
|
+
# 2) segm test for /users/
|
|
51
|
+
# 3) segm test for /users/:id
|
|
52
|
+
# (jump to list index = 5)
|
|
53
|
+
# 4) full test for /users/:id => here we are!
|
|
54
|
+
|
|
55
|
+
class RouteSet
|
|
56
|
+
def recognize_path(path, environment={})
|
|
57
|
+
result = recognize_optimized(path, environment) and return result
|
|
58
|
+
|
|
59
|
+
# Route was not recognized. Try to find out why (maybe wrong verb).
|
|
60
|
+
allows = HTTP_METHODS.select { |verb| routes.find { |r| r.recognize(path, :method => verb) } }
|
|
61
|
+
|
|
62
|
+
if environment[:method] && !HTTP_METHODS.include?(environment[:method])
|
|
63
|
+
raise NotImplemented.new(*allows)
|
|
64
|
+
elsif !allows.empty?
|
|
65
|
+
raise MethodNotAllowed.new(*allows)
|
|
66
|
+
else
|
|
67
|
+
raise RoutingError, "No route matches #{path.inspect} with #{environment.inspect}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def recognize_optimized(path, env)
|
|
72
|
+
write_recognize_optimized
|
|
73
|
+
recognize_optimized(path, env)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def write_recognize_optimized
|
|
77
|
+
tree = segment_tree(routes)
|
|
78
|
+
body = generate_code(tree)
|
|
79
|
+
instance_eval %{
|
|
80
|
+
def recognize_optimized(path, env)
|
|
81
|
+
segments = to_plain_segments(path)
|
|
82
|
+
index = #{body}
|
|
83
|
+
return nil unless index
|
|
84
|
+
while index < routes.size
|
|
85
|
+
result = routes[index].recognize(path, env) and return result
|
|
86
|
+
index += 1
|
|
87
|
+
end
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
}, __FILE__, __LINE__
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def segment_tree(routes)
|
|
94
|
+
tree = [0]
|
|
95
|
+
|
|
96
|
+
i = -1
|
|
97
|
+
routes.each do |route|
|
|
98
|
+
i += 1
|
|
99
|
+
# not fast, but runs only once
|
|
100
|
+
segments = to_plain_segments(route.segments.inject("") { |str,s| str << s.to_s })
|
|
101
|
+
|
|
102
|
+
node = tree
|
|
103
|
+
segments.each do |seg|
|
|
104
|
+
seg = :dynamic if seg && seg[0] == ?:
|
|
105
|
+
node << [seg, [i]] if node.empty? || node[node.size - 1][0] != seg
|
|
106
|
+
node = node[node.size - 1][1]
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
tree
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def generate_code(list, padding=' ', level = 0)
|
|
113
|
+
# a digit
|
|
114
|
+
return padding + "#{list[0]}\n" if list.size == 1 && !(Array === list[0])
|
|
115
|
+
|
|
116
|
+
body = padding + "(seg = segments[#{level}]; \n"
|
|
117
|
+
|
|
118
|
+
i = 0
|
|
119
|
+
was_nil = false
|
|
120
|
+
list.each do |item|
|
|
121
|
+
if Array === item
|
|
122
|
+
i += 1
|
|
123
|
+
start = (i == 1)
|
|
124
|
+
final = (i == list.size)
|
|
125
|
+
tag, sub = item
|
|
126
|
+
if tag == :dynamic
|
|
127
|
+
body += padding + "#{start ? 'if' : 'elsif'} true\n"
|
|
128
|
+
body += generate_code(sub, padding + " ", level + 1)
|
|
129
|
+
break
|
|
130
|
+
elsif tag == nil && !was_nil
|
|
131
|
+
was_nil = true
|
|
132
|
+
body += padding + "#{start ? 'if' : 'elsif'} seg.nil?\n"
|
|
133
|
+
body += generate_code(sub, padding + " ", level + 1)
|
|
134
|
+
else
|
|
135
|
+
body += padding + "#{start ? 'if' : 'elsif'} seg == '#{tag}'\n"
|
|
136
|
+
body += generate_code(sub, padding + " ", level + 1)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
body += padding + "else\n"
|
|
141
|
+
body += padding + " #{list[0]}\n"
|
|
142
|
+
body += padding + "end)\n"
|
|
143
|
+
body
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# this must be really fast
|
|
147
|
+
def to_plain_segments(str)
|
|
148
|
+
str = str.dup
|
|
149
|
+
str.sub!(/^\/+/,'')
|
|
150
|
+
str.sub!(/\/+$/,'')
|
|
151
|
+
segments = str.split(/\.[^\/]+\/+|\/+|\.[^\/]+\Z/) # cut off ".format" also
|
|
152
|
+
segments << nil
|
|
153
|
+
segments
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
module ActionController
|
|
2
|
+
module Routing
|
|
3
|
+
class Route #:nodoc:
|
|
4
|
+
attr_accessor :segments, :requirements, :conditions, :optimise
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@segments = []
|
|
8
|
+
@requirements = {}
|
|
9
|
+
@conditions = {}
|
|
10
|
+
@optimise = true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Indicates whether the routes should be optimised with the string interpolation
|
|
14
|
+
# version of the named routes methods.
|
|
15
|
+
def optimise?
|
|
16
|
+
@optimise && ActionController::Base::optimise_named_routes
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def segment_keys
|
|
20
|
+
segments.collect do |segment|
|
|
21
|
+
segment.key if segment.respond_to? :key
|
|
22
|
+
end.compact
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Write and compile a +generate+ method for this Route.
|
|
26
|
+
def write_generation
|
|
27
|
+
# Build the main body of the generation
|
|
28
|
+
body = "expired = false\n#{generation_extraction}\n#{generation_structure}"
|
|
29
|
+
|
|
30
|
+
# If we have conditions that must be tested first, nest the body inside an if
|
|
31
|
+
body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements
|
|
32
|
+
args = "options, hash, expire_on = {}"
|
|
33
|
+
|
|
34
|
+
# Nest the body inside of a def block, and then compile it.
|
|
35
|
+
raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend"
|
|
36
|
+
instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
|
|
37
|
+
|
|
38
|
+
# expire_on.keys == recall.keys; in other words, the keys in the expire_on hash
|
|
39
|
+
# are the same as the keys that were recalled from the previous request. Thus,
|
|
40
|
+
# we can use the expire_on.keys to determine which keys ought to be used to build
|
|
41
|
+
# the query string. (Never use keys from the recalled request when building the
|
|
42
|
+
# query string.)
|
|
43
|
+
|
|
44
|
+
method_decl = "def generate(#{args})\npath, hash = generate_raw(options, hash, expire_on)\nappend_query_string(path, hash, extra_keys(options))\nend"
|
|
45
|
+
instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
|
|
46
|
+
|
|
47
|
+
method_decl = "def generate_extras(#{args})\npath, hash = generate_raw(options, hash, expire_on)\n[path, extra_keys(options)]\nend"
|
|
48
|
+
instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
|
|
49
|
+
raw_method
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Build several lines of code that extract values from the options hash. If any
|
|
53
|
+
# of the values are missing or rejected then a return will be executed.
|
|
54
|
+
def generation_extraction
|
|
55
|
+
segments.collect do |segment|
|
|
56
|
+
segment.extraction_code
|
|
57
|
+
end.compact * "\n"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Produce a condition expression that will check the requirements of this route
|
|
61
|
+
# upon generation.
|
|
62
|
+
def generation_requirements
|
|
63
|
+
requirement_conditions = requirements.collect do |key, req|
|
|
64
|
+
if req.is_a? Regexp
|
|
65
|
+
value_regexp = Regexp.new "\\A#{req.to_s}\\Z"
|
|
66
|
+
"hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]"
|
|
67
|
+
else
|
|
68
|
+
"hash[:#{key}] == #{req.inspect}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
requirement_conditions * ' && ' unless requirement_conditions.empty?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def generation_structure
|
|
75
|
+
segments.last.string_structure segments[0..-2]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Write and compile a +recognize+ method for this Route.
|
|
79
|
+
def write_recognition
|
|
80
|
+
# Create an if structure to extract the params from a match if it occurs.
|
|
81
|
+
body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams"
|
|
82
|
+
body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend"
|
|
83
|
+
|
|
84
|
+
# Build the method declaration and compile it
|
|
85
|
+
method_decl = "def recognize(path, env={})\n#{body}\nend"
|
|
86
|
+
instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
|
|
87
|
+
method_decl
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Plugins may override this method to add other conditions, like checks on
|
|
91
|
+
# host, subdomain, and so forth. Note that changes here only affect route
|
|
92
|
+
# recognition, not generation.
|
|
93
|
+
def recognition_conditions
|
|
94
|
+
result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"]
|
|
95
|
+
result << "conditions[:method] === env[:method]" if conditions[:method]
|
|
96
|
+
result
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Build the regular expression pattern that will match this route.
|
|
100
|
+
def recognition_pattern(wrap = true)
|
|
101
|
+
pattern = ''
|
|
102
|
+
segments.reverse_each do |segment|
|
|
103
|
+
pattern = segment.build_pattern pattern
|
|
104
|
+
end
|
|
105
|
+
wrap ? ("\\A" + pattern + "\\Z") : pattern
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Write the code to extract the parameters from a matched route.
|
|
109
|
+
def recognition_extraction
|
|
110
|
+
next_capture = 1
|
|
111
|
+
extraction = segments.collect do |segment|
|
|
112
|
+
x = segment.match_extraction(next_capture)
|
|
113
|
+
next_capture += Regexp.new(segment.regexp_chunk).number_of_captures
|
|
114
|
+
x
|
|
115
|
+
end
|
|
116
|
+
extraction.compact
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Write the real generation implementation and then resend the message.
|
|
120
|
+
def generate(options, hash, expire_on = {})
|
|
121
|
+
write_generation
|
|
122
|
+
generate options, hash, expire_on
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def generate_extras(options, hash, expire_on = {})
|
|
126
|
+
write_generation
|
|
127
|
+
generate_extras options, hash, expire_on
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Generate the query string with any extra keys in the hash and append
|
|
131
|
+
# it to the given path, returning the new path.
|
|
132
|
+
def append_query_string(path, hash, query_keys=nil)
|
|
133
|
+
return nil unless path
|
|
134
|
+
query_keys ||= extra_keys(hash)
|
|
135
|
+
"#{path}#{build_query_string(hash, query_keys)}"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Determine which keys in the given hash are "extra". Extra keys are
|
|
139
|
+
# those that were not used to generate a particular route. The extra
|
|
140
|
+
# keys also do not include those recalled from the prior request, nor
|
|
141
|
+
# do they include any keys that were implied in the route (like a
|
|
142
|
+
# <tt>:controller</tt> that is required, but not explicitly used in the
|
|
143
|
+
# text of the route.)
|
|
144
|
+
def extra_keys(hash, recall={})
|
|
145
|
+
(hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Build a query string from the keys of the given hash. If +only_keys+
|
|
149
|
+
# is given (as an array), only the keys indicated will be used to build
|
|
150
|
+
# the query string. The query string will correctly build array parameter
|
|
151
|
+
# values.
|
|
152
|
+
def build_query_string(hash, only_keys = nil)
|
|
153
|
+
elements = []
|
|
154
|
+
|
|
155
|
+
(only_keys || hash.keys).each do |key|
|
|
156
|
+
if value = hash[key]
|
|
157
|
+
elements << value.to_query(key)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
elements.empty? ? '' : "?#{elements.sort * '&'}"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Write the real recognition implementation and then resend the message.
|
|
165
|
+
def recognize(path, environment={})
|
|
166
|
+
write_recognition
|
|
167
|
+
recognize path, environment
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# A route's parameter shell contains parameter values that are not in the
|
|
171
|
+
# route's path, but should be placed in the recognized hash.
|
|
172
|
+
#
|
|
173
|
+
# For example, +{:controller => 'pages', :action => 'show'} is the shell for the route:
|
|
174
|
+
#
|
|
175
|
+
# map.connect '/page/:id', :controller => 'pages', :action => 'show', :id => /\d+/
|
|
176
|
+
#
|
|
177
|
+
def parameter_shell
|
|
178
|
+
@parameter_shell ||= returning({}) do |shell|
|
|
179
|
+
requirements.each do |key, requirement|
|
|
180
|
+
shell[key] = requirement unless requirement.is_a? Regexp
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Return an array containing all the keys that are used in this route. This
|
|
186
|
+
# includes keys that appear inside the path, and keys that have requirements
|
|
187
|
+
# placed upon them.
|
|
188
|
+
def significant_keys
|
|
189
|
+
@significant_keys ||= returning [] do |sk|
|
|
190
|
+
segments.each { |segment| sk << segment.key if segment.respond_to? :key }
|
|
191
|
+
sk.concat requirements.keys
|
|
192
|
+
sk.uniq!
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Return a hash of key/value pairs representing the keys in the route that
|
|
197
|
+
# have defaults, or which are specified by non-regexp requirements.
|
|
198
|
+
def defaults
|
|
199
|
+
@defaults ||= returning({}) do |hash|
|
|
200
|
+
segments.each do |segment|
|
|
201
|
+
next unless segment.respond_to? :default
|
|
202
|
+
hash[segment.key] = segment.default unless segment.default.nil?
|
|
203
|
+
end
|
|
204
|
+
requirements.each do |key,req|
|
|
205
|
+
next if Regexp === req || req.nil?
|
|
206
|
+
hash[key] = req
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def matches_controller_and_action?(controller, action)
|
|
212
|
+
unless defined? @matching_prepared
|
|
213
|
+
@controller_requirement = requirement_for(:controller)
|
|
214
|
+
@action_requirement = requirement_for(:action)
|
|
215
|
+
@matching_prepared = true
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
(@controller_requirement.nil? || @controller_requirement === controller) &&
|
|
219
|
+
(@action_requirement.nil? || @action_requirement === action)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def to_s
|
|
223
|
+
@to_s ||= begin
|
|
224
|
+
segs = segments.inject("") { |str,s| str << s.to_s }
|
|
225
|
+
"%-6s %-40s %s" % [(conditions[:method] || :any).to_s.upcase, segs, requirements.inspect]
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
protected
|
|
230
|
+
def requirement_for(key)
|
|
231
|
+
return requirements[key] if requirements.key? key
|
|
232
|
+
segments.each do |segment|
|
|
233
|
+
return segment.regexp if segment.respond_to?(:key) && segment.key == key
|
|
234
|
+
end
|
|
235
|
+
nil
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|