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.
Files changed (210) hide show
  1. data/CHANGELOG +149 -7
  2. data/MIT-LICENSE +1 -1
  3. data/README +1 -1
  4. data/Rakefile +5 -6
  5. data/lib/action_controller.rb +2 -2
  6. data/lib/action_controller/assertions/model_assertions.rb +2 -1
  7. data/lib/action_controller/assertions/response_assertions.rb +4 -2
  8. data/lib/action_controller/assertions/routing_assertions.rb +3 -3
  9. data/lib/action_controller/assertions/selector_assertions.rb +30 -27
  10. data/lib/action_controller/assertions/tag_assertions.rb +3 -3
  11. data/lib/action_controller/base.rb +103 -129
  12. data/lib/action_controller/benchmarking.rb +3 -3
  13. data/lib/action_controller/caching.rb +41 -652
  14. data/lib/action_controller/caching/actions.rb +144 -0
  15. data/lib/action_controller/caching/fragments.rb +138 -0
  16. data/lib/action_controller/caching/pages.rb +154 -0
  17. data/lib/action_controller/caching/sql_cache.rb +18 -0
  18. data/lib/action_controller/caching/sweeping.rb +97 -0
  19. data/lib/action_controller/cgi_ext/cookie.rb +27 -23
  20. data/lib/action_controller/cgi_ext/stdinput.rb +1 -0
  21. data/lib/action_controller/cgi_process.rb +6 -4
  22. data/lib/action_controller/components.rb +7 -6
  23. data/lib/action_controller/cookies.rb +31 -19
  24. data/lib/action_controller/dispatcher.rb +51 -84
  25. data/lib/action_controller/filters.rb +295 -421
  26. data/lib/action_controller/flash.rb +1 -6
  27. data/lib/action_controller/headers.rb +31 -0
  28. data/lib/action_controller/helpers.rb +26 -9
  29. data/lib/action_controller/http_authentication.rb +1 -1
  30. data/lib/action_controller/integration.rb +65 -13
  31. data/lib/action_controller/layout.rb +24 -39
  32. data/lib/action_controller/mime_responds.rb +7 -3
  33. data/lib/action_controller/mime_type.rb +25 -9
  34. data/lib/action_controller/mime_types.rb +1 -1
  35. data/lib/action_controller/polymorphic_routes.rb +32 -17
  36. data/lib/action_controller/record_identifier.rb +10 -4
  37. data/lib/action_controller/request.rb +46 -30
  38. data/lib/action_controller/request_forgery_protection.rb +10 -9
  39. data/lib/action_controller/request_profiler.rb +29 -8
  40. data/lib/action_controller/rescue.rb +24 -24
  41. data/lib/action_controller/resources.rb +66 -23
  42. data/lib/action_controller/response.rb +2 -2
  43. data/lib/action_controller/routing.rb +113 -1229
  44. data/lib/action_controller/routing/builder.rb +204 -0
  45. data/lib/action_controller/{routing_optimisation.rb → routing/optimisations.rb} +13 -12
  46. data/lib/action_controller/routing/recognition_optimisation.rb +158 -0
  47. data/lib/action_controller/routing/route.rb +240 -0
  48. data/lib/action_controller/routing/route_set.rb +435 -0
  49. data/lib/action_controller/routing/routing_ext.rb +46 -0
  50. data/lib/action_controller/routing/segments.rb +283 -0
  51. data/lib/action_controller/session/active_record_store.rb +13 -8
  52. data/lib/action_controller/session/cookie_store.rb +20 -17
  53. data/lib/action_controller/session_management.rb +10 -3
  54. data/lib/action_controller/streaming.rb +45 -31
  55. data/lib/action_controller/test_case.rb +33 -23
  56. data/lib/action_controller/test_process.rb +39 -35
  57. data/lib/action_controller/url_rewriter.rb +18 -12
  58. data/lib/action_controller/vendor/html-scanner/html/tokenizer.rb +1 -1
  59. data/lib/action_pack.rb +1 -1
  60. data/lib/action_pack/version.rb +2 -2
  61. data/lib/action_view.rb +11 -3
  62. data/lib/action_view/base.rb +73 -390
  63. data/lib/action_view/helpers/active_record_helper.rb +83 -62
  64. data/lib/action_view/helpers/asset_tag_helper.rb +101 -44
  65. data/lib/action_view/helpers/atom_feed_helper.rb +35 -7
  66. data/lib/action_view/helpers/benchmark_helper.rb +5 -3
  67. data/lib/action_view/helpers/cache_helper.rb +3 -2
  68. data/lib/action_view/helpers/capture_helper.rb +1 -2
  69. data/lib/action_view/helpers/date_helper.rb +104 -82
  70. data/lib/action_view/helpers/form_helper.rb +148 -75
  71. data/lib/action_view/helpers/form_options_helper.rb +44 -23
  72. data/lib/action_view/helpers/form_tag_helper.rb +22 -13
  73. data/lib/action_view/helpers/javascripts/controls.js +1 -1
  74. data/lib/action_view/helpers/javascripts/dragdrop.js +1 -1
  75. data/lib/action_view/helpers/javascripts/effects.js +1 -1
  76. data/lib/action_view/helpers/number_helper.rb +10 -3
  77. data/lib/action_view/helpers/prototype_helper.rb +61 -29
  78. data/lib/action_view/helpers/record_tag_helper.rb +3 -3
  79. data/lib/action_view/helpers/sanitize_helper.rb +23 -17
  80. data/lib/action_view/helpers/scriptaculous_helper.rb +86 -60
  81. data/lib/action_view/helpers/text_helper.rb +153 -125
  82. data/lib/action_view/helpers/url_helper.rb +83 -28
  83. data/lib/action_view/inline_template.rb +20 -0
  84. data/lib/action_view/partial_template.rb +70 -0
  85. data/lib/action_view/partials.rb +31 -73
  86. data/lib/action_view/template.rb +127 -0
  87. data/lib/action_view/template_error.rb +8 -7
  88. data/lib/action_view/template_finder.rb +177 -0
  89. data/lib/action_view/template_handler.rb +18 -1
  90. data/lib/action_view/template_handlers/builder.rb +10 -2
  91. data/lib/action_view/template_handlers/compilable.rb +128 -0
  92. data/lib/action_view/template_handlers/erb.rb +37 -2
  93. data/lib/action_view/template_handlers/rjs.rb +14 -1
  94. data/lib/action_view/test_case.rb +58 -0
  95. data/test/abstract_unit.rb +1 -1
  96. data/test/active_record_unit.rb +3 -6
  97. data/test/activerecord/active_record_store_test.rb +1 -2
  98. data/test/activerecord/render_partial_with_record_identification_test.rb +158 -41
  99. data/test/adv_attr_test.rb +20 -0
  100. data/test/controller/action_pack_assertions_test.rb +16 -19
  101. data/test/controller/addresses_render_test.rb +1 -1
  102. data/test/controller/assert_select_test.rb +13 -6
  103. data/test/controller/base_test.rb +48 -2
  104. data/test/controller/benchmark_test.rb +1 -2
  105. data/test/controller/caching_test.rb +282 -21
  106. data/test/controller/capture_test.rb +1 -1
  107. data/test/controller/cgi_test.rb +1 -1
  108. data/test/controller/components_test.rb +1 -1
  109. data/test/controller/content_type_test.rb +2 -2
  110. data/test/controller/cookie_test.rb +13 -2
  111. data/test/controller/custom_handler_test.rb +14 -10
  112. data/test/controller/deprecation/deprecated_base_methods_test.rb +1 -1
  113. data/test/controller/dispatcher_test.rb +31 -49
  114. data/test/controller/fake_controllers.rb +17 -0
  115. data/test/controller/fake_models.rb +6 -0
  116. data/test/controller/filter_params_test.rb +14 -8
  117. data/test/controller/filters_test.rb +44 -16
  118. data/test/controller/flash_test.rb +2 -2
  119. data/test/controller/header_test.rb +14 -0
  120. data/test/controller/helper_test.rb +19 -15
  121. data/test/controller/html-scanner/document_test.rb +1 -2
  122. data/test/controller/html-scanner/node_test.rb +1 -2
  123. data/test/controller/html-scanner/sanitizer_test.rb +8 -5
  124. data/test/controller/html-scanner/tag_node_test.rb +1 -2
  125. data/test/controller/html-scanner/text_node_test.rb +2 -3
  126. data/test/controller/html-scanner/tokenizer_test.rb +8 -2
  127. data/test/controller/http_authentication_test.rb +1 -1
  128. data/test/controller/integration_test.rb +14 -16
  129. data/test/controller/integration_upload_test.rb +43 -0
  130. data/test/controller/layout_test.rb +26 -6
  131. data/test/controller/mime_responds_test.rb +39 -7
  132. data/test/controller/mime_type_test.rb +29 -5
  133. data/test/controller/new_render_test.rb +105 -34
  134. data/test/controller/polymorphic_routes_test.rb +32 -20
  135. data/test/controller/record_identifier_test.rb +38 -2
  136. data/test/controller/redirect_test.rb +21 -1
  137. data/test/controller/render_test.rb +59 -15
  138. data/test/controller/request_forgery_protection_test.rb +92 -5
  139. data/test/controller/request_test.rb +64 -6
  140. data/test/controller/rescue_test.rb +22 -6
  141. data/test/controller/resources_test.rb +102 -14
  142. data/test/controller/routing_test.rb +231 -19
  143. data/test/controller/selector_test.rb +2 -2
  144. data/test/controller/send_file_test.rb +14 -3
  145. data/test/controller/session/cookie_store_test.rb +16 -4
  146. data/test/controller/session/mem_cache_store_test.rb +3 -4
  147. data/test/controller/session_fixation_test.rb +1 -1
  148. data/test/controller/session_management_test.rb +23 -1
  149. data/test/controller/test_test.rb +39 -18
  150. data/test/controller/url_rewriter_test.rb +35 -1
  151. data/test/controller/verification_test.rb +1 -1
  152. data/test/controller/view_paths_test.rb +15 -12
  153. data/test/controller/webservice_test.rb +48 -3
  154. data/test/fixtures/bad_customers/_bad_customer.html.erb +1 -0
  155. data/test/fixtures/company.rb +1 -0
  156. data/test/fixtures/customers/_customer.html.erb +1 -0
  157. data/test/fixtures/db_definitions/sqlite.sql +6 -0
  158. data/test/fixtures/functional_caching/_partial.erb +3 -0
  159. data/test/fixtures/functional_caching/fragment_cached.html.erb +2 -0
  160. data/test/fixtures/functional_caching/html_fragment_cached_with_partial.html.erb +1 -0
  161. data/test/fixtures/functional_caching/js_fragment_cached_with_partial.js.rjs +1 -0
  162. data/test/fixtures/good_customers/_good_customer.html.erb +1 -0
  163. data/test/fixtures/mascot.rb +3 -0
  164. data/test/fixtures/mascots.yml +4 -0
  165. data/test/fixtures/mascots/_mascot.html.erb +1 -0
  166. data/test/fixtures/multipart/boundary_problem_file +10 -0
  167. data/test/fixtures/public/javascripts/application.js +1 -0
  168. data/test/fixtures/public/javascripts/controls.js +1 -0
  169. data/test/fixtures/public/javascripts/dragdrop.js +1 -0
  170. data/test/fixtures/public/javascripts/effects.js +1 -0
  171. data/test/fixtures/public/javascripts/prototype.js +1 -0
  172. data/test/fixtures/public/javascripts/version.1.0.js +1 -0
  173. data/test/fixtures/public/stylesheets/version.1.0.css +1 -0
  174. data/test/fixtures/reply.rb +1 -0
  175. data/test/fixtures/shared.html.erb +1 -0
  176. data/test/fixtures/symlink_parent/symlinked_layout.erb +5 -0
  177. data/test/fixtures/test/_customer_counter.erb +1 -0
  178. data/test/fixtures/test/_form.erb +1 -0
  179. data/test/fixtures/test/_labelling_form.erb +1 -0
  180. data/test/fixtures/test/_raise.html.erb +1 -0
  181. data/test/fixtures/test/greeting.js.rjs +1 -0
  182. data/test/fixtures/topics/_topic.html.erb +1 -0
  183. data/test/template/active_record_helper_test.rb +25 -8
  184. data/test/template/asset_tag_helper_test.rb +100 -17
  185. data/test/template/atom_feed_helper_test.rb +29 -1
  186. data/test/template/benchmark_helper_test.rb +10 -22
  187. data/test/template/date_helper_test.rb +455 -153
  188. data/test/template/erb_util_test.rb +10 -42
  189. data/test/template/form_helper_test.rb +192 -66
  190. data/test/template/form_options_helper_test.rb +19 -8
  191. data/test/template/form_tag_helper_test.rb +11 -8
  192. data/test/template/javascript_helper_test.rb +3 -9
  193. data/test/template/number_helper_test.rb +6 -3
  194. data/test/template/prototype_helper_test.rb +27 -40
  195. data/test/template/record_tag_helper_test.rb +54 -0
  196. data/test/template/sanitize_helper_test.rb +5 -6
  197. data/test/template/scriptaculous_helper_test.rb +7 -13
  198. data/test/template/tag_helper_test.rb +3 -6
  199. data/test/template/template_finder_test.rb +73 -0
  200. data/test/template/template_object_test.rb +95 -0
  201. data/test/template/test_test.rb +56 -0
  202. data/test/template/text_helper_test.rb +46 -33
  203. data/test/template/url_helper_test.rb +8 -10
  204. metadata +65 -12
  205. data/lib/action_view/compiled_templates.rb +0 -69
  206. data/test/action_view_test.rb +0 -44
  207. data/test/activerecord/fixtures_test.rb +0 -24
  208. data/test/controller/fragment_store_setting_test.rb +0 -47
  209. data/test/template/compiled_templates_test.rb +0 -197
  210. 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, :requirements matching, defaults providing
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
- # :requirements, we can just build up a string and return it.
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 :url optimisation pending proper solution to
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
- # If the user calls person_url(@person), we can simply
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 # *always* be a
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