actionpack 8.0.3 → 8.1.0.rc1

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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +335 -158
  3. data/lib/abstract_controller/asset_paths.rb +4 -2
  4. data/lib/abstract_controller/base.rb +12 -3
  5. data/lib/abstract_controller/caching.rb +6 -3
  6. data/lib/abstract_controller/logger.rb +2 -1
  7. data/lib/action_controller/api.rb +1 -0
  8. data/lib/action_controller/base.rb +2 -1
  9. data/lib/action_controller/caching.rb +1 -2
  10. data/lib/action_controller/form_builder.rb +1 -1
  11. data/lib/action_controller/log_subscriber.rb +18 -3
  12. data/lib/action_controller/metal/allow_browser.rb +1 -1
  13. data/lib/action_controller/metal/conditional_get.rb +25 -0
  14. data/lib/action_controller/metal/data_streaming.rb +1 -3
  15. data/lib/action_controller/metal/exceptions.rb +5 -0
  16. data/lib/action_controller/metal/flash.rb +1 -4
  17. data/lib/action_controller/metal/head.rb +3 -1
  18. data/lib/action_controller/metal/live.rb +9 -18
  19. data/lib/action_controller/metal/permissions_policy.rb +9 -0
  20. data/lib/action_controller/metal/rate_limiting.rb +30 -9
  21. data/lib/action_controller/metal/redirecting.rb +104 -9
  22. data/lib/action_controller/metal/renderers.rb +27 -6
  23. data/lib/action_controller/metal/rendering.rb +7 -1
  24. data/lib/action_controller/metal/request_forgery_protection.rb +18 -10
  25. data/lib/action_controller/metal/rescue.rb +9 -0
  26. data/lib/action_controller/railtie.rb +27 -8
  27. data/lib/action_controller/structured_event_subscriber.rb +107 -0
  28. data/lib/action_dispatch/http/cache.rb +111 -1
  29. data/lib/action_dispatch/http/filter_parameters.rb +5 -3
  30. data/lib/action_dispatch/http/mime_negotiation.rb +55 -1
  31. data/lib/action_dispatch/http/mime_types.rb +1 -0
  32. data/lib/action_dispatch/http/param_builder.rb +28 -27
  33. data/lib/action_dispatch/http/parameters.rb +3 -3
  34. data/lib/action_dispatch/http/permissions_policy.rb +4 -0
  35. data/lib/action_dispatch/http/query_parser.rb +12 -10
  36. data/lib/action_dispatch/http/request.rb +10 -5
  37. data/lib/action_dispatch/http/response.rb +16 -3
  38. data/lib/action_dispatch/http/url.rb +110 -14
  39. data/lib/action_dispatch/journey/gtg/simulator.rb +33 -12
  40. data/lib/action_dispatch/journey/gtg/transition_table.rb +33 -41
  41. data/lib/action_dispatch/journey/nodes/node.rb +2 -1
  42. data/lib/action_dispatch/journey/route.rb +45 -31
  43. data/lib/action_dispatch/journey/router/utils.rb +8 -14
  44. data/lib/action_dispatch/journey/router.rb +59 -81
  45. data/lib/action_dispatch/journey/routes.rb +7 -0
  46. data/lib/action_dispatch/journey/visitors.rb +55 -23
  47. data/lib/action_dispatch/journey/visualizer/fsm.js +4 -6
  48. data/lib/action_dispatch/log_subscriber.rb +7 -3
  49. data/lib/action_dispatch/middleware/cookies.rb +4 -2
  50. data/lib/action_dispatch/middleware/debug_exceptions.rb +7 -1
  51. data/lib/action_dispatch/middleware/debug_view.rb +11 -0
  52. data/lib/action_dispatch/middleware/exception_wrapper.rb +11 -5
  53. data/lib/action_dispatch/middleware/executor.rb +12 -2
  54. data/lib/action_dispatch/middleware/public_exceptions.rb +1 -5
  55. data/lib/action_dispatch/middleware/remote_ip.rb +9 -4
  56. data/lib/action_dispatch/middleware/session/cache_store.rb +17 -0
  57. data/lib/action_dispatch/middleware/templates/rescues/_copy_button.html.erb +1 -0
  58. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +3 -2
  59. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +9 -5
  60. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +1 -0
  61. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +1 -0
  62. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +4 -0
  63. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +3 -0
  64. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +50 -0
  65. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +1 -0
  66. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -0
  67. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -0
  68. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -0
  69. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -0
  70. data/lib/action_dispatch/railtie.rb +14 -2
  71. data/lib/action_dispatch/routing/inspector.rb +79 -56
  72. data/lib/action_dispatch/routing/mapper.rb +323 -173
  73. data/lib/action_dispatch/routing/redirection.rb +10 -7
  74. data/lib/action_dispatch/routing/route_set.rb +2 -4
  75. data/lib/action_dispatch/structured_event_subscriber.rb +20 -0
  76. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +2 -2
  77. data/lib/action_dispatch/testing/assertions/response.rb +14 -0
  78. data/lib/action_dispatch/testing/assertions/routing.rb +11 -3
  79. data/lib/action_dispatch/testing/integration.rb +1 -1
  80. data/lib/action_dispatch.rb +8 -0
  81. data/lib/action_pack/gem_version.rb +3 -3
  82. metadata +13 -10
@@ -16,15 +16,27 @@ module ActionDispatch
16
16
  @param_depth_limit = param_depth_limit
17
17
  end
18
18
 
19
- cattr_accessor :ignore_leading_brackets
20
-
21
- LEADING_BRACKETS_COMPAT = defined?(::Rack::RELEASE) && ::Rack::RELEASE.to_s.start_with?("2.")
22
-
23
19
  cattr_accessor :default
24
20
  self.default = make_default(100)
25
21
 
26
22
  class << self
27
23
  delegate :from_query_string, :from_pairs, :from_hash, to: :default
24
+
25
+ def ignore_leading_brackets
26
+ ActionDispatch.deprecator.warn <<~MSG
27
+ ActionDispatch::ParamBuilder.ignore_leading_brackets is deprecated and have no effect and will be removed in Rails 8.2.
28
+ MSG
29
+
30
+ @ignore_leading_brackets
31
+ end
32
+
33
+ def ignore_leading_brackets=(value)
34
+ ActionDispatch.deprecator.warn <<~MSG
35
+ ActionDispatch::ParamBuilder.ignore_leading_brackets is deprecated and have no effect and will be removed in Rails 8.2.
36
+ MSG
37
+
38
+ @ignore_leading_brackets = value
39
+ end
28
40
  end
29
41
 
30
42
  def from_query_string(qs, separator: nil, encoding_template: nil)
@@ -69,30 +81,15 @@ module ActionDispatch
69
81
  # nil name, treat same as empty string (required by tests)
70
82
  k = after = ""
71
83
  elsif depth == 0
72
- if ignore_leading_brackets || (ignore_leading_brackets.nil? && LEADING_BRACKETS_COMPAT)
73
- # Rack 2 compatible behavior, ignore leading brackets
74
- if name =~ /\A[\[\]]*([^\[\]]+)\]*/
75
- k = $1
76
- after = $' || ""
77
-
78
- if !ignore_leading_brackets && (k != $& || !after.empty? && !after.start_with?("["))
79
- ActionDispatch.deprecator.warn("Skipping over leading brackets in parameter name #{name.inspect} is deprecated and will parse differently in Rails 8.1 or Rack 3.0.")
80
- end
81
- else
82
- k = name
83
- after = ""
84
- end
84
+ # Start of parsing, don't treat [] or [ at start of string specially
85
+ if start = name.index("[", 1)
86
+ # Start of parameter nesting, use part before brackets as key
87
+ k = name[0, start]
88
+ after = name[start, name.length]
85
89
  else
86
- # Start of parsing, don't treat [] or [ at start of string specially
87
- if start = name.index("[", 1)
88
- # Start of parameter nesting, use part before brackets as key
89
- k = name[0, start]
90
- after = name[start, name.length]
91
- else
92
- # Plain parameter with no nesting
93
- k = name
94
- after = ""
95
- end
90
+ # Plain parameter with no nesting
91
+ k = name
92
+ after = ""
96
93
  end
97
94
  elsif name.start_with?("[]")
98
95
  # Array nesting
@@ -111,6 +108,10 @@ module ActionDispatch
111
108
 
112
109
  return if k.empty?
113
110
 
111
+ unless k.valid_encoding?
112
+ raise InvalidParameterError, "Invalid encoding for parameter: #{k}"
113
+ end
114
+
114
115
  if depth == 0 && String === v
115
116
  # We have to wait until we've found the top part of the name,
116
117
  # because that's what the encoding template is configured with
@@ -65,14 +65,14 @@ module ActionDispatch
65
65
  alias :params :parameters
66
66
 
67
67
  def path_parameters=(parameters) # :nodoc:
68
- delete_header("action_dispatch.request.parameters")
68
+ @env.delete("action_dispatch.request.parameters")
69
69
 
70
70
  parameters = Request::Utils.set_binary_encoding(self, parameters, parameters[:controller], parameters[:action])
71
71
  # If any of the path parameters has an invalid encoding then raise since it's
72
72
  # likely to trigger errors further on.
73
73
  Request::Utils.check_param_encoding(parameters)
74
74
 
75
- set_header PARAMETERS_KEY, parameters
75
+ @env[PARAMETERS_KEY] = parameters
76
76
  rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
77
77
  raise ActionController::BadRequest.new("Invalid path parameters: #{e.message}")
78
78
  end
@@ -82,7 +82,7 @@ module ActionDispatch
82
82
  #
83
83
  # { action: "my_action", controller: "my_controller" }
84
84
  def path_parameters
85
- get_header(PARAMETERS_KEY) || set_header(PARAMETERS_KEY, {})
85
+ @env[PARAMETERS_KEY] ||= {}
86
86
  end
87
87
 
88
88
  private
@@ -186,4 +186,8 @@ module ActionDispatch # :nodoc:
186
186
  end
187
187
  end
188
188
  end
189
+
190
+ ActiveSupport.on_load(:action_dispatch_request) do
191
+ include ActionDispatch::PermissionsPolicy::Request
192
+ end
189
193
  end
@@ -6,12 +6,21 @@ require "rack"
6
6
  module ActionDispatch
7
7
  class QueryParser
8
8
  DEFAULT_SEP = /& */n
9
- COMPAT_SEP = /[&;] */n
10
9
  COMMON_SEP = { ";" => /; */n, ";," => /[;,] */n, "&" => /& */n, "&;" => /[&;] */n }
11
10
 
12
- cattr_accessor :strict_query_string_separator
11
+ def self.strict_query_string_separator
12
+ ActionDispatch.deprecator.warn <<~MSG
13
+ The `strict_query_string_separator` configuration is deprecated have no effect and will be removed in Rails 8.2.
14
+ MSG
15
+ @strict_query_string_separator
16
+ end
13
17
 
14
- SEMICOLON_COMPAT = defined?(::Rack::QueryParser::DEFAULT_SEP) && ::Rack::QueryParser::DEFAULT_SEP.to_s.include?(";")
18
+ def self.strict_query_string_separator=(value)
19
+ ActionDispatch.deprecator.warn <<~MSG
20
+ The `strict_query_string_separator` configuration is deprecated have no effect and will be removed in Rails 8.2.
21
+ MSG
22
+ @strict_query_string_separator = value
23
+ end
15
24
 
16
25
  #--
17
26
  # Note this departs from WHATWG's specified parsing algorithm by
@@ -25,13 +34,6 @@ module ActionDispatch
25
34
  splitter =
26
35
  if separator
27
36
  COMMON_SEP[separator] || /[#{separator}] */n
28
- elsif strict_query_string_separator
29
- DEFAULT_SEP
30
- elsif SEMICOLON_COMPAT && s.include?(";")
31
- if strict_query_string_separator.nil?
32
- ActionDispatch.deprecator.warn("Using semicolon as a query string separator is deprecated and will not be supported in Rails 8.1 or Rack 3.0. Use `&` instead.")
33
- end
34
- COMPAT_SEP
35
37
  else
36
38
  DEFAULT_SEP
37
39
  end
@@ -25,7 +25,6 @@ module ActionDispatch
25
25
  include ActionDispatch::Http::FilterParameters
26
26
  include ActionDispatch::Http::URL
27
27
  include ActionDispatch::ContentSecurityPolicy::Request
28
- include ActionDispatch::PermissionsPolicy::Request
29
28
  include Rack::Request::Env
30
29
 
31
30
  autoload :Session, "action_dispatch/request/session"
@@ -139,7 +138,7 @@ module ActionDispatch
139
138
 
140
139
  # Populate the HTTP method lookup cache.
141
140
  HTTP_METHODS.each { |method|
142
- HTTP_METHOD_LOOKUP[method] = method.downcase.underscore.to_sym
141
+ HTTP_METHOD_LOOKUP[method] = method.downcase.tap { |m| m.tr!("-", "_") }.to_sym
143
142
  }
144
143
 
145
144
  alias raw_request_method request_method # :nodoc:
@@ -158,11 +157,17 @@ module ActionDispatch
158
157
  #
159
158
  # request.route_uri_pattern # => "/:controller(/:action(/:id))(.:format)"
160
159
  def route_uri_pattern
161
- get_header("action_dispatch.route_uri_pattern")
160
+ unless pattern = get_header("action_dispatch.route_uri_pattern")
161
+ route = get_header("action_dispatch.route")
162
+ return if route.nil?
163
+ pattern = route.path.spec.to_s
164
+ set_header("action_dispatch.route_uri_pattern", pattern)
165
+ end
166
+ pattern
162
167
  end
163
168
 
164
- def route_uri_pattern=(pattern) # :nodoc:
165
- set_header("action_dispatch.route_uri_pattern", pattern)
169
+ def route=(route) # :nodoc:
170
+ @env["action_dispatch.route"] = route
166
171
  end
167
172
 
168
173
  def routes # :nodoc:
@@ -277,14 +277,27 @@ module ActionDispatch # :nodoc:
277
277
  # Sets the HTTP response's content MIME type. For example, in the controller you
278
278
  # could write this:
279
279
  #
280
- # response.content_type = "text/plain"
280
+ # response.content_type = "text/html"
281
+ #
282
+ # This method also accepts a symbol with the extension of the MIME type:
283
+ #
284
+ # response.content_type = :html
281
285
  #
282
286
  # If a character set has been defined for this response (see #charset=) then the
283
287
  # character set information will also be included in the content type
284
288
  # information.
285
289
  def content_type=(content_type)
286
- return unless content_type
287
- new_header_info = parse_content_type(content_type.to_s)
290
+ case content_type
291
+ when NilClass
292
+ return
293
+ when Symbol
294
+ mime_type = Mime[content_type]
295
+ raise ArgumentError, "Unknown MIME type #{content_type}" unless mime_type
296
+ new_header_info = ContentTypeHeader.new(mime_type.to_s)
297
+ else
298
+ new_header_info = parse_content_type(content_type.to_s)
299
+ end
300
+
288
301
  prev_header_info = parsed_content_type_header
289
302
  charset = new_header_info.charset || prev_header_info.charset
290
303
  charset ||= self.class.default_charset unless prev_header_info.mime_type
@@ -11,8 +11,105 @@ module ActionDispatch
11
11
  HOST_REGEXP = /(^[^:]+:\/\/)?(\[[^\]]+\]|[^:]+)(?::(\d+$))?/
12
12
  PROTOCOL_REGEXP = /^([^:]+)(:)?(\/\/)?$/
13
13
 
14
+ # DomainExtractor provides utility methods for extracting domain and subdomain
15
+ # information from host strings. This module is used internally by Action Dispatch
16
+ # to parse host names and separate the domain from subdomains based on the
17
+ # top-level domain (TLD) length.
18
+ #
19
+ # The module assumes a standard domain structure where domains consist of:
20
+ # - Subdomains (optional, can be multiple levels)
21
+ # - Domain name
22
+ # - Top-level domain (TLD, can be multiple levels like .co.uk)
23
+ #
24
+ # For example, in "api.staging.example.co.uk":
25
+ # - Subdomains: ["api", "staging"]
26
+ # - Domain: "example.co.uk" (with tld_length=2)
27
+ # - TLD: "co.uk"
28
+ module DomainExtractor
29
+ extend self
30
+
31
+ # Extracts the domain part from a host string, including the specified
32
+ # number of top-level domain components.
33
+ #
34
+ # The domain includes the main domain name plus the TLD components.
35
+ # The +tld_length+ parameter specifies how many components from the right
36
+ # should be considered part of the TLD.
37
+ #
38
+ # ==== Parameters
39
+ #
40
+ # [+host+]
41
+ # The host string to extract the domain from.
42
+ #
43
+ # [+tld_length+]
44
+ # The number of domain components that make up the TLD. For example,
45
+ # use 1 for ".com" or 2 for ".co.uk".
46
+ #
47
+ # ==== Examples
48
+ #
49
+ # # Standard TLD (tld_length = 1)
50
+ # DomainExtractor.domain_from("www.example.com", 1)
51
+ # # => "example.com"
52
+ #
53
+ # # Country-code TLD (tld_length = 2)
54
+ # DomainExtractor.domain_from("www.example.co.uk", 2)
55
+ # # => "example.co.uk"
56
+ #
57
+ # # Multiple subdomains
58
+ # DomainExtractor.domain_from("api.staging.myapp.herokuapp.com", 1)
59
+ # # => "herokuapp.com"
60
+ #
61
+ # # Single component (returns the host itself)
62
+ # DomainExtractor.domain_from("localhost", 1)
63
+ # # => "localhost"
64
+ def domain_from(host, tld_length)
65
+ host.split(".").last(1 + tld_length).join(".")
66
+ end
67
+
68
+ # Extracts the subdomain components from a host string as an Array.
69
+ #
70
+ # Returns all the components that come before the domain and TLD parts.
71
+ # The +tld_length+ parameter is used to determine where the domain begins
72
+ # so that everything before it is considered a subdomain.
73
+ #
74
+ # ==== Parameters
75
+ #
76
+ # [+host+]
77
+ # The host string to extract subdomains from.
78
+ #
79
+ # [+tld_length+]
80
+ # The number of domain components that make up the TLD. This affects
81
+ # where the domain boundary is calculated.
82
+ #
83
+ # ==== Examples
84
+ #
85
+ # # Standard TLD (tld_length = 1)
86
+ # DomainExtractor.subdomains_from("www.example.com", 1)
87
+ # # => ["www"]
88
+ #
89
+ # # Country-code TLD (tld_length = 2)
90
+ # DomainExtractor.subdomains_from("api.staging.example.co.uk", 2)
91
+ # # => ["api", "staging"]
92
+ #
93
+ # # No subdomains
94
+ # DomainExtractor.subdomains_from("example.com", 1)
95
+ # # => []
96
+ #
97
+ # # Single subdomain with complex TLD
98
+ # DomainExtractor.subdomains_from("www.mysite.co.uk", 2)
99
+ # # => ["www"]
100
+ #
101
+ # # Multiple levels of subdomains
102
+ # DomainExtractor.subdomains_from("dev.api.staging.example.com", 1)
103
+ # # => ["dev", "api", "staging"]
104
+ def subdomains_from(host, tld_length)
105
+ parts = host.split(".")
106
+ parts[0..-(tld_length + 2)]
107
+ end
108
+ end
109
+
14
110
  mattr_accessor :secure_protocol, default: false
15
111
  mattr_accessor :tld_length, default: 1
112
+ mattr_accessor :domain_extractor, default: DomainExtractor
16
113
 
17
114
  class << self
18
115
  # Returns the domain part of a host given the domain level.
@@ -96,34 +193,33 @@ module ActionDispatch
96
193
  end
97
194
 
98
195
  def extract_domain_from(host, tld_length)
99
- host.split(".").last(1 + tld_length).join(".")
196
+ domain_extractor.domain_from(host, tld_length)
100
197
  end
101
198
 
102
199
  def extract_subdomains_from(host, tld_length)
103
- parts = host.split(".")
104
- parts[0..-(tld_length + 2)]
200
+ domain_extractor.subdomains_from(host, tld_length)
105
201
  end
106
202
 
107
203
  def build_host_url(host, port, protocol, options, path)
108
204
  if match = host.match(HOST_REGEXP)
109
- protocol ||= match[1] unless protocol == false
110
- host = match[2]
111
- port = match[3] unless options.key? :port
205
+ protocol_from_host = match[1] if protocol.nil?
206
+ host = match[2]
207
+ port = match[3] unless options.key? :port
112
208
  end
113
209
 
114
- protocol = normalize_protocol protocol
210
+ protocol = protocol_from_host || normalize_protocol(protocol).dup
115
211
  host = normalize_host(host, options)
212
+ port = normalize_port(port, protocol)
116
213
 
117
- result = protocol.dup
214
+ result = protocol
118
215
 
119
216
  if options[:user] && options[:password]
120
217
  result << "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@"
121
218
  end
122
219
 
123
220
  result << host
124
- normalize_port(port, protocol) { |normalized_port|
125
- result << ":#{normalized_port}"
126
- }
221
+
222
+ result << ":" << port.to_s if port
127
223
 
128
224
  result.concat path
129
225
  end
@@ -169,11 +265,11 @@ module ActionDispatch
169
265
  return unless port
170
266
 
171
267
  case protocol
172
- when "//" then yield port
268
+ when "//" then port
173
269
  when "https://"
174
- yield port unless port.to_i == 443
270
+ port unless port.to_i == 443
175
271
  else
176
- yield port unless port.to_i == 80
272
+ port unless port.to_i == 80
177
273
  end
178
274
  end
179
275
  end
@@ -2,8 +2,6 @@
2
2
 
3
3
  # :markup: markdown
4
4
 
5
- require "strscan"
6
-
7
5
  module ActionDispatch
8
6
  module Journey # :nodoc:
9
7
  module GTG # :nodoc:
@@ -16,7 +14,13 @@ module ActionDispatch
16
14
  end
17
15
 
18
16
  class Simulator # :nodoc:
19
- INITIAL_STATE = [ [0, nil] ].freeze
17
+ STATIC_TOKENS = Array.new(64)
18
+ STATIC_TOKENS[".".ord] = "."
19
+ STATIC_TOKENS["/".ord] = "/"
20
+ STATIC_TOKENS["?".ord] = "?"
21
+ STATIC_TOKENS.freeze
22
+
23
+ INITIAL_STATE = [0, nil].freeze
20
24
 
21
25
  attr_reader :tt
22
26
 
@@ -25,21 +29,38 @@ module ActionDispatch
25
29
  end
26
30
 
27
31
  def memos(string)
28
- input = StringScanner.new(string)
29
32
  state = INITIAL_STATE
30
- start_index = 0
31
33
 
32
- while sym = input.scan(%r([/.?]|[^/.?]+))
33
- end_index = start_index + sym.length
34
+ pos = 0
35
+ eos = string.bytesize
36
+
37
+ while pos < eos
38
+ start_index = pos
39
+ pos += 1
34
40
 
35
- state = tt.move(state, string, start_index, end_index)
41
+ if (token = STATIC_TOKENS[string.getbyte(start_index)])
42
+ state = tt.move(state, string, token, start_index, false)
43
+ else
44
+ while pos < eos && STATIC_TOKENS[string.getbyte(pos)].nil?
45
+ pos += 1
46
+ end
36
47
 
37
- start_index = end_index
48
+ token = string.byteslice(start_index, pos - start_index)
49
+ state = tt.move(state, string, token, start_index, true)
50
+ end
38
51
  end
39
52
 
40
- acceptance_states = state.each_with_object([]) do |s_d, memos|
41
- s, idx = s_d
42
- memos.concat(tt.memo(s)) if idx.nil? && tt.accepting?(s)
53
+ acceptance_states = []
54
+ states_count = state.size
55
+ i = 0
56
+ while i < states_count
57
+ if state[i + 1].nil?
58
+ s = state[i]
59
+ if tt.accepting?(s)
60
+ acceptance_states.concat(tt.memo(s))
61
+ end
62
+ end
63
+ i += 2
43
64
  end
44
65
 
45
66
  acceptance_states.empty? ? yield : acceptance_states
@@ -13,7 +13,6 @@ module ActionDispatch
13
13
  attr_reader :memos
14
14
 
15
15
  DEFAULT_EXP = /[^.\/?]+/
16
- DEFAULT_EXP_ANCHORED = /\A#{DEFAULT_EXP}\Z/
17
16
 
18
17
  def initialize
19
18
  @stdparam_states = {}
@@ -47,25 +46,27 @@ module ActionDispatch
47
46
  Array(t)
48
47
  end
49
48
 
50
- def move(t, full_string, start_index, end_index)
49
+ def move(t, full_string, token, start_index, token_matches_default)
51
50
  return [] if t.empty?
52
51
 
53
52
  next_states = []
54
53
 
55
- tok = full_string.slice(start_index, end_index - start_index)
56
- token_matches_default_component = DEFAULT_EXP_ANCHORED.match?(tok)
57
-
58
- t.each { |s, previous_start|
54
+ transitions_count = t.size
55
+ i = 0
56
+ while i < transitions_count
57
+ s = t[i]
58
+ previous_start = t[i + 1]
59
59
  if previous_start.nil?
60
60
  # In the simple case of a "default" param regex do this fast-path and add all
61
61
  # next states.
62
- if token_matches_default_component && states = @stdparam_states[s]
63
- states.each { |re, v| next_states << [v, nil].freeze if !v.nil? }
62
+ if token_matches_default && std_state = @stdparam_states[s]
63
+ next_states << std_state << nil
64
64
  end
65
65
 
66
66
  # When we have a literal string, we can just pull the next state
67
67
  if states = @string_states[s]
68
- next_states << [states[tok], nil].freeze unless states[tok].nil?
68
+ state = states[token]
69
+ next_states << state << nil unless state.nil?
69
70
  end
70
71
  end
71
72
 
@@ -80,19 +81,21 @@ module ActionDispatch
80
81
  previous_start
81
82
  end
82
83
 
83
- slice_length = end_index - slice_start
84
+ slice_length = start_index + token.length - slice_start
84
85
  curr_slice = full_string.slice(slice_start, slice_length)
85
86
 
86
87
  states.each { |re, v|
87
88
  # if we match, we can try moving past this
88
- next_states << [v, nil].freeze if !v.nil? && re.match?(curr_slice)
89
+ next_states << v << nil if !v.nil? && re.match?(curr_slice)
89
90
  }
90
91
 
91
92
  # and regardless, we must continue accepting tokens and retrying this regexp. we
92
93
  # need to remember where we started as well so we can take bigger slices.
93
- next_states << [s, slice_start].freeze
94
+ next_states << s << slice_start
94
95
  end
95
- }
96
+
97
+ i += 2
98
+ end
96
99
 
97
100
  next_states
98
101
  end
@@ -163,54 +166,43 @@ module ActionDispatch
163
166
  end
164
167
 
165
168
  def []=(from, to, sym)
166
- to_mappings = states_hash_for(sym)[from] ||= {}
167
169
  case sym
170
+ when String, Symbol
171
+ to_mapping = @string_states[from] ||= {}
172
+ # account for symbols in the constraints the same as strings
173
+ to_mapping[sym.to_s] = to
168
174
  when Regexp
169
- # we must match the whole string to a token boundary
170
175
  if sym == DEFAULT_EXP
171
- sym = DEFAULT_EXP_ANCHORED
176
+ @stdparam_states[from] = to
172
177
  else
173
- sym = /\A#{sym}\Z/
178
+ to_mapping = @regexp_states[from] ||= {}
179
+ # we must match the whole string to a token boundary
180
+ to_mapping[/\A#{sym}\Z/] = to
174
181
  end
175
- when Symbol
176
- # account for symbols in the constraints the same as strings
177
- sym = sym.to_s
182
+ else
183
+ raise ArgumentError, "unknown symbol: %s" % sym.class
178
184
  end
179
- to_mappings[sym] = to
180
185
  end
181
186
 
182
187
  def states
183
188
  ss = @string_states.keys + @string_states.values.flat_map(&:values)
184
- ps = @stdparam_states.keys + @stdparam_states.values.flat_map(&:values)
189
+ ps = @stdparam_states.keys + @stdparam_states.values
185
190
  rs = @regexp_states.keys + @regexp_states.values.flat_map(&:values)
186
191
  (ss + ps + rs).uniq
187
192
  end
188
193
 
189
194
  def transitions
195
+ # double escaped because dot evaluates escapes
196
+ default_exp_anchored = "\\\\A#{DEFAULT_EXP.source}\\\\Z"
197
+
190
198
  @string_states.flat_map { |from, hash|
191
199
  hash.map { |s, to| [from, s, to] }
192
- } + @stdparam_states.flat_map { |from, hash|
193
- hash.map { |s, to| [from, s, to] }
200
+ } + @stdparam_states.map { |from, to|
201
+ [from, default_exp_anchored, to]
194
202
  } + @regexp_states.flat_map { |from, hash|
195
- hash.map { |s, to| [from, s, to] }
203
+ hash.map { |r, to| [from, r.source.gsub("\\") { "\\\\" }, to] }
196
204
  }
197
205
  end
198
-
199
- private
200
- def states_hash_for(sym)
201
- case sym
202
- when String, Symbol
203
- @string_states
204
- when Regexp
205
- if sym == DEFAULT_EXP
206
- @stdparam_states
207
- else
208
- @regexp_states
209
- end
210
- else
211
- raise ArgumentError, "unknown symbol: %s" % sym.class
212
- end
213
- end
214
206
  end
215
207
  end
216
208
  end
@@ -74,6 +74,7 @@ module ActionDispatch
74
74
  def initialize(left)
75
75
  @left = left
76
76
  @memo = nil
77
+ @to_s = nil
77
78
  end
78
79
 
79
80
  def each(&block)
@@ -81,7 +82,7 @@ module ActionDispatch
81
82
  end
82
83
 
83
84
  def to_s
84
- Visitors::String::INSTANCE.accept(self, "")
85
+ @to_s ||= Visitors::String::INSTANCE.accept(self, "".dup).freeze
85
86
  end
86
87
 
87
88
  def to_dot