sidekiq 7.3.9 → 8.0.8

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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +116 -0
  3. data/README.md +16 -13
  4. data/bin/sidekiqload +10 -10
  5. data/bin/webload +69 -0
  6. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +104 -58
  7. data/lib/sidekiq/api.rb +124 -39
  8. data/lib/sidekiq/capsule.rb +6 -6
  9. data/lib/sidekiq/cli.rb +15 -19
  10. data/lib/sidekiq/client.rb +28 -17
  11. data/lib/sidekiq/component.rb +42 -3
  12. data/lib/sidekiq/config.rb +23 -20
  13. data/lib/sidekiq/embedded.rb +2 -1
  14. data/lib/sidekiq/iterable_job.rb +1 -0
  15. data/lib/sidekiq/job/iterable.rb +44 -16
  16. data/lib/sidekiq/job.rb +2 -2
  17. data/lib/sidekiq/job_logger.rb +4 -4
  18. data/lib/sidekiq/job_retry.rb +33 -10
  19. data/lib/sidekiq/job_util.rb +5 -1
  20. data/lib/sidekiq/launcher.rb +2 -1
  21. data/lib/sidekiq/loader.rb +57 -0
  22. data/lib/sidekiq/logger.rb +25 -69
  23. data/lib/sidekiq/manager.rb +0 -1
  24. data/lib/sidekiq/metrics/query.rb +71 -45
  25. data/lib/sidekiq/metrics/shared.rb +8 -5
  26. data/lib/sidekiq/metrics/tracking.rb +12 -7
  27. data/lib/sidekiq/middleware/current_attributes.rb +11 -19
  28. data/lib/sidekiq/paginator.rb +8 -1
  29. data/lib/sidekiq/processor.rb +21 -14
  30. data/lib/sidekiq/profiler.rb +72 -0
  31. data/lib/sidekiq/rails.rb +46 -67
  32. data/lib/sidekiq/redis_client_adapter.rb +0 -1
  33. data/lib/sidekiq/redis_connection.rb +14 -3
  34. data/lib/sidekiq/testing.rb +3 -3
  35. data/lib/sidekiq/transaction_aware_client.rb +13 -5
  36. data/lib/sidekiq/version.rb +2 -2
  37. data/lib/sidekiq/web/action.rb +146 -83
  38. data/lib/sidekiq/web/application.rb +353 -332
  39. data/lib/sidekiq/web/config.rb +120 -0
  40. data/lib/sidekiq/web/helpers.rb +57 -27
  41. data/lib/sidekiq/web/router.rb +60 -76
  42. data/lib/sidekiq/web.rb +51 -156
  43. data/lib/sidekiq.rb +6 -1
  44. data/sidekiq.gemspec +6 -6
  45. data/web/assets/images/logo.png +0 -0
  46. data/web/assets/images/status.png +0 -0
  47. data/web/assets/javascripts/application.js +26 -26
  48. data/web/assets/javascripts/base-charts.js +30 -16
  49. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  50. data/web/assets/javascripts/dashboard.js +1 -1
  51. data/web/assets/javascripts/metrics.js +16 -34
  52. data/web/assets/stylesheets/style.css +759 -0
  53. data/web/locales/ar.yml +1 -0
  54. data/web/locales/cs.yml +1 -0
  55. data/web/locales/da.yml +1 -0
  56. data/web/locales/de.yml +1 -0
  57. data/web/locales/el.yml +1 -0
  58. data/web/locales/en.yml +6 -0
  59. data/web/locales/es.yml +24 -2
  60. data/web/locales/fa.yml +1 -0
  61. data/web/locales/fr.yml +1 -0
  62. data/web/locales/gd.yml +1 -0
  63. data/web/locales/he.yml +1 -0
  64. data/web/locales/hi.yml +1 -0
  65. data/web/locales/it.yml +8 -0
  66. data/web/locales/ja.yml +1 -0
  67. data/web/locales/ko.yml +1 -0
  68. data/web/locales/lt.yml +1 -0
  69. data/web/locales/nb.yml +1 -0
  70. data/web/locales/nl.yml +1 -0
  71. data/web/locales/pl.yml +1 -0
  72. data/web/locales/{pt-br.yml → pt-BR.yml} +2 -1
  73. data/web/locales/pt.yml +1 -0
  74. data/web/locales/ru.yml +1 -0
  75. data/web/locales/sv.yml +1 -0
  76. data/web/locales/ta.yml +1 -0
  77. data/web/locales/tr.yml +1 -0
  78. data/web/locales/uk.yml +6 -5
  79. data/web/locales/ur.yml +1 -0
  80. data/web/locales/vi.yml +1 -0
  81. data/web/locales/{zh-cn.yml → zh-CN.yml} +85 -73
  82. data/web/locales/{zh-tw.yml → zh-TW.yml} +2 -1
  83. data/web/views/_footer.erb +31 -33
  84. data/web/views/_job_info.erb +91 -89
  85. data/web/views/_metrics_period_select.erb +13 -10
  86. data/web/views/_nav.erb +14 -21
  87. data/web/views/_paging.erb +23 -21
  88. data/web/views/_poll_link.erb +2 -2
  89. data/web/views/_summary.erb +16 -16
  90. data/web/views/busy.erb +124 -122
  91. data/web/views/dashboard.erb +62 -66
  92. data/web/views/dead.erb +31 -27
  93. data/web/views/filtering.erb +3 -3
  94. data/web/views/layout.erb +13 -29
  95. data/web/views/metrics.erb +75 -81
  96. data/web/views/metrics_for_job.erb +45 -46
  97. data/web/views/morgue.erb +61 -70
  98. data/web/views/profiles.erb +43 -0
  99. data/web/views/queue.erb +54 -52
  100. data/web/views/queues.erb +43 -41
  101. data/web/views/retries.erb +66 -75
  102. data/web/views/retry.erb +32 -27
  103. data/web/views/scheduled.erb +59 -55
  104. data/web/views/scheduled_job_info.erb +1 -1
  105. metadata +26 -25
  106. data/web/assets/stylesheets/application-dark.css +0 -147
  107. data/web/assets/stylesheets/application-rtl.css +0 -163
  108. data/web/assets/stylesheets/application.css +0 -759
  109. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  110. data/web/assets/stylesheets/bootstrap.css +0 -5
  111. data/web/views/_status.erb +0 -4
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/web/csrf_protection"
4
+
5
+ module Sidekiq
6
+ class Web
7
+ ##
8
+ # Configure the Sidekiq::Web instance in this process:
9
+ #
10
+ # require "sidekiq/web"
11
+ # Sidekiq::Web.configure do |config|
12
+ # config.register(MyExtension, name: "myext", tab: "TabName", index: "tabpage/")
13
+ # end
14
+ #
15
+ # This should go in your `config/routes.rb` or similar. It
16
+ # does not belong in your initializer since Web should not be
17
+ # loaded in some processes (like an actual Sidekiq process).
18
+ # See `examples/webui-ext` for a sample web extension.
19
+ class Config
20
+ extend Forwardable
21
+
22
+ OPTIONS = {
23
+ # By default we support direct uploads to p.f.c since the UI is a JS SPA
24
+ # and very difficult for us to vendor or provide ourselves. If you are worried
25
+ # about data security and wish to self-host, you can change these URLs.
26
+ profile_view_url: "https://profiler.firefox.com/public/%s",
27
+ profile_store_url: "https://api.profiler.firefox.com/compressed-store",
28
+ # Will be false in Sidekiq 9.0.
29
+ # CSRF is unnecessary if you are using SameSite=(Strict|Lax) cookies.
30
+ csrf: true
31
+ }
32
+
33
+ ##
34
+ # Allows users to add custom rows to all of the Job
35
+ # tables, e.g. Retries, Dead, Scheduled, with custom
36
+ # links to other systems, see _job_info.erb and test
37
+ # in web_test.rb
38
+ #
39
+ # Sidekiq::Web.configure do |cfg|
40
+ # cfg.custom_job_info_rows << JobLogLink.new
41
+ # end
42
+ #
43
+ # class JobLogLink
44
+ # def add_pair(job)
45
+ # yield "External Logs", "<a href='https://example.com/logs/#{job.jid}'>Logs for #{job.jid}</a>"
46
+ # end
47
+ # end
48
+ attr_accessor :custom_job_info_rows
49
+
50
+ attr_reader :tabs
51
+ attr_reader :locales
52
+ attr_reader :views
53
+ attr_reader :middlewares
54
+
55
+ # Adds the "Back to App" link in the header
56
+ attr_accessor :app_url
57
+
58
+ def initialize
59
+ @options = OPTIONS.dup
60
+ @locales = LOCALES
61
+ @views = VIEWS
62
+ @tabs = DEFAULT_TABS.dup
63
+ @middlewares = []
64
+ @custom_job_info_rows = []
65
+ end
66
+
67
+ def_delegators :@options, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :dig
68
+
69
+ def use(*args, &block)
70
+ middlewares << [args, block]
71
+ end
72
+
73
+ # Register a class as a Sidekiq Web UI extension. The class should
74
+ # provide one or more tabs which map to an index route. Options:
75
+ #
76
+ # @param extclass [Class] Class which contains the HTTP actions, required
77
+ # @param name [String] the name of the extension, used to namespace assets
78
+ # @param tab [String | Array] labels(s) of the UI tabs
79
+ # @param index [String | Array] index route(s) for each tab
80
+ # @param root_dir [String] directory location to find assets, locales and views, typically `web/` within the gemfile
81
+ # @param asset_paths [Array] one or more directories under {root}/assets/{name} to be publicly served, e.g. ["js", "css", "img"]
82
+ # @param cache_for [Integer] amount of time to cache assets, default one day
83
+ #
84
+ # Web extensions will have a root `web/` directory with `locales/`, `assets/`
85
+ # and `views/` subdirectories.
86
+ def register_extension(extclass, name:, tab:, index:, root_dir: nil, cache_for: 86400, asset_paths: nil)
87
+ tab = Array(tab)
88
+ index = Array(index)
89
+ tab.zip(index).each do |tab, index|
90
+ tabs[tab] = index
91
+ end
92
+ if root_dir
93
+ locdir = File.join(root_dir, "locales")
94
+ locales << locdir if File.directory?(locdir)
95
+
96
+ if asset_paths && name
97
+ # if you have {root}/assets/{name}/js/scripts.js
98
+ # and {root}/assets/{name}/css/styles.css
99
+ # you would pass in:
100
+ # asset_paths: ["js", "css"]
101
+ # See script_tag and style_tag in web/helpers.rb
102
+ assdir = File.join(root_dir, "assets")
103
+ assurls = Array(asset_paths).map { |x| "/#{name}/#{x}" }
104
+ assetprops = {
105
+ urls: assurls,
106
+ root: assdir,
107
+ cascade: true
108
+ }
109
+ assetprops[:header_rules] = [[:all, {"cache-control" => "private, max-age=#{cache_for.to_i}"}]] if cache_for
110
+ middlewares << [[Rack::Static, assetprops], nil]
111
+ end
112
+ end
113
+
114
+ yield self if block_given?
115
+ extclass.registered(Web::Application)
116
+ end
117
+ alias_method :register, :register_extension
118
+ end
119
+ end
120
+ end
@@ -1,14 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "uri"
4
- require "set"
5
4
  require "yaml"
6
- require "cgi"
5
+ require "cgi/escape"
7
6
 
8
7
  module Sidekiq
9
8
  # These methods are available to pages within the Web UI and UI extensions.
10
9
  # They are not public APIs for applications to use.
11
10
  module WebHelpers
11
+ def store_name
12
+ hash = redis_info
13
+ return "Dragonfly" if hash.has_key?("dragonfly_version")
14
+ return "Valkey" if hash.has_key?("valkey_version")
15
+ "Redis"
16
+ end
17
+
18
+ def store_version
19
+ hash = redis_info
20
+ return hash["dragonfly_version"] if hash.has_key?("dragonfly_version")
21
+ return hash["valkey_version"] if hash.has_key?("valkey_version")
22
+ hash["redis_version"]
23
+ end
24
+
12
25
  def style_tag(location, **kwargs)
13
26
  global = location.match?(/:\/\//)
14
27
  location = root_path + location if !global && !location.start_with?(root_path)
@@ -19,7 +32,9 @@ module Sidekiq
19
32
  nonce: csp_nonce,
20
33
  href: location
21
34
  }
22
- html_tag(:link, attrs.merge(kwargs))
35
+ add_to_head do
36
+ html_tag(:link, attrs.merge(kwargs))
37
+ end
23
38
  end
24
39
 
25
40
  def script_tag(location, **kwargs)
@@ -36,7 +51,7 @@ module Sidekiq
36
51
  # NB: keys and values are not escaped; do not allow user input
37
52
  # in the attributes
38
53
  private def html_tag(tagname, attrs)
39
- s = +"<#{tagname}"
54
+ s = "<#{tagname}"
40
55
  attrs.each_pair do |k, v|
41
56
  next unless v
42
57
  s << " #{k}=\"#{v}\""
@@ -56,9 +71,9 @@ module Sidekiq
56
71
 
57
72
  # Allow sidekiq-web extensions to add locale paths
58
73
  # so extensions can be localized
59
- @@strings[lang] ||= settings.locales.each_with_object({}) do |path, global|
74
+ @@strings[lang] ||= config.locales.each_with_object({}) do |path, global|
60
75
  find_locale_files(lang).each do |file|
61
- strs = YAML.safe_load(File.read(file))
76
+ strs = YAML.safe_load_file(file)
62
77
  global.merge!(strs[lang])
63
78
  end
64
79
  end
@@ -83,7 +98,7 @@ module Sidekiq
83
98
  end
84
99
 
85
100
  def locale_files
86
- @@locale_files ||= settings.locales.flat_map { |path|
101
+ @@locale_files ||= config.locales.flat_map { |path|
87
102
  Dir["#{path}/*.yml"]
88
103
  }
89
104
  end
@@ -96,6 +111,10 @@ module Sidekiq
96
111
  locale_files.select { |file| file =~ /\/#{lang}\.yml$/ }
97
112
  end
98
113
 
114
+ def language_name(locale)
115
+ strings(locale).fetch("LanguageName", locale)
116
+ end
117
+
99
118
  def search(jobset, substr)
100
119
  resultset = jobset.scan(substr).to_a
101
120
  @current_page = 1
@@ -117,7 +136,7 @@ module Sidekiq
117
136
 
118
137
  def display_tags(job, within = "retries")
119
138
  job.tags.map { |tag|
120
- "<span class='label label-info jobtag'>#{filter_link(tag, within)}</span>"
139
+ "<span class='label label-info jobtag jobtag-#{Rack::Utils.escape_html(tag)}'>#{filter_link(tag, within)}</span>"
121
140
  }.join(" ")
122
141
  end
123
142
 
@@ -146,10 +165,13 @@ module Sidekiq
146
165
  text_direction == "rtl"
147
166
  end
148
167
 
149
- # See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
168
+ # See https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.4
169
+ # Returns an array of language tags ordered by their quality value
170
+ #
171
+ # Inspiration taken from https://github.com/iain/http_accept_language/blob/master/lib/http_accept_language/parser.rb
150
172
  def user_preferred_languages
151
173
  languages = env["HTTP_ACCEPT_LANGUAGE"]
152
- languages.to_s.downcase.gsub(/\s+/, "").split(",").map { |language|
174
+ languages.to_s.gsub(/\s+/, "").split(",").map { |language|
153
175
  locale, quality = language.split(";q=", 2)
154
176
  locale = nil if locale == "*" # Ignore wildcards
155
177
  quality = quality ? quality.to_f : 1.0
@@ -161,22 +183,30 @@ module Sidekiq
161
183
 
162
184
  # Given an Accept-Language header like "fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4,ru;q=0.2"
163
185
  # this method will try to best match the available locales to the user's preferred languages.
164
- #
165
- # Inspiration taken from https://github.com/iain/http_accept_language/blob/master/lib/http_accept_language/parser.rb
166
186
  def locale
167
187
  # session[:locale] is set via the locale selector from the footer
168
188
  @locale ||= if (l = session&.fetch(:locale, nil)) && available_locales.include?(l)
169
189
  l
170
190
  else
171
- matched_locale = user_preferred_languages.map { |preferred|
172
- preferred_language = preferred.split("-", 2).first
191
+ matched_locale = nil
192
+ # Attempt to find a case-insensitive exact match first
193
+ user_preferred_languages.each do |preferred|
194
+ # We only care about the language and primary subtag
195
+ # "en-GB-oxendict" becomes "en-GB"
196
+ language_tag = preferred.split("-")[0..1].join("-")
197
+ matched_locale = available_locales.find { |available_locale| available_locale.casecmp?(language_tag) }
198
+ break if matched_locale
199
+ end
173
200
 
174
- lang_group = available_locales.select { |available|
175
- preferred_language == available.split("-", 2).first
176
- }
201
+ return matched_locale if matched_locale
177
202
 
178
- lang_group.find { |lang| lang == preferred } || lang_group.min_by(&:length)
179
- }.compact.first
203
+ # Find the first base language match
204
+ # "en-US,es-MX;q=0.9" matches "en"
205
+ user_preferred_languages.each do |preferred|
206
+ base_language = preferred.split("-", 2).first
207
+ matched_locale = available_locales.find { |available_locale| available_locale.casecmp?(base_language) }
208
+ break if matched_locale
209
+ end
180
210
 
181
211
  matched_locale || "en"
182
212
  end
@@ -202,7 +232,7 @@ module Sidekiq
202
232
  end
203
233
 
204
234
  def sort_direction_label
205
- (params[:direction] == "asc") ? "&uarr;" : "&darr;"
235
+ (url_params("direction") == "asc") ? "&uarr;" : "&darr;"
206
236
  end
207
237
 
208
238
  def workset
@@ -245,7 +275,7 @@ module Sidekiq
245
275
  end
246
276
 
247
277
  def redis_info
248
- Sidekiq.default_configuration.redis_info
278
+ @info ||= Sidekiq.default_configuration.redis_info
249
279
  end
250
280
 
251
281
  def root_path
@@ -269,8 +299,8 @@ module Sidekiq
269
299
  "#{score}-#{job["jid"]}"
270
300
  end
271
301
 
272
- def parse_params(params)
273
- score, jid = params.split("-", 2)
302
+ def parse_key(key)
303
+ score, jid = key.split("-", 2)
274
304
  [score.to_f, jid]
275
305
  end
276
306
 
@@ -280,11 +310,11 @@ module Sidekiq
280
310
  def qparams(options)
281
311
  stringified_options = options.transform_keys(&:to_s)
282
312
 
283
- to_query_string(params.merge(stringified_options))
313
+ to_query_string(request.params.merge(stringified_options))
284
314
  end
285
315
 
286
- def to_query_string(params)
287
- params.map { |key, value|
316
+ def to_query_string(hash)
317
+ hash.map { |key, value|
288
318
  SAFE_QPARAMS.include?(key) ? "#{key}=#{CGI.escape(value.to_s)}" : next
289
319
  }.compact.join("&")
290
320
  end
@@ -347,7 +377,7 @@ module Sidekiq
347
377
  elsif rss_kb < 10_000_000
348
378
  "#{number_with_delimiter((rss_kb / 1024.0).to_i)} MB"
349
379
  else
350
- "#{number_with_delimiter((rss_kb / (1024.0 * 1024.0)), precision: 1)} GB"
380
+ "#{number_with_delimiter(rss_kb / (1024.0 * 1024.0), precision: 1)} GB"
351
381
  end
352
382
  end
353
383
 
@@ -3,104 +3,88 @@
3
3
  require "rack"
4
4
 
5
5
  module Sidekiq
6
- module WebRouter
7
- GET = "GET"
8
- DELETE = "DELETE"
9
- POST = "POST"
10
- PUT = "PUT"
11
- PATCH = "PATCH"
12
- HEAD = "HEAD"
13
-
14
- ROUTE_PARAMS = "rack.route_params"
15
- REQUEST_METHOD = "REQUEST_METHOD"
16
- PATH_INFO = "PATH_INFO"
17
-
18
- def head(path, &block)
19
- route(HEAD, path, &block)
20
- end
6
+ class Web
7
+ # Provides an API to declare endpoints, along with a match
8
+ # API to dynamically route a request to an endpoint.
9
+ module Router
10
+ def head(path, &) = route(:head, path, &)
21
11
 
22
- def get(path, &block)
23
- route(GET, path, &block)
24
- end
12
+ def get(path, &) = route(:get, path, &)
25
13
 
26
- def post(path, &block)
27
- route(POST, path, &block)
28
- end
14
+ def post(path, &) = route(:post, path, &)
29
15
 
30
- def put(path, &block)
31
- route(PUT, path, &block)
32
- end
16
+ def put(path, &) = route(:put, path, &)
33
17
 
34
- def patch(path, &block)
35
- route(PATCH, path, &block)
36
- end
18
+ def patch(path, &) = route(:patch, path, &)
37
19
 
38
- def delete(path, &block)
39
- route(DELETE, path, &block)
40
- end
20
+ def delete(path, &) = route(:delete, path, &)
41
21
 
42
- def route(*methods, path, &block)
43
- @routes ||= {GET => [], POST => [], PUT => [], PATCH => [], DELETE => [], HEAD => []}
44
-
45
- methods.each do |method|
46
- method = method.to_s.upcase
47
- @routes[method] << WebRoute.new(method, path, block)
22
+ def route(*methods, path, &block)
23
+ methods.each do |method|
24
+ raise ArgumentError, "Invalid method #{method}. Must be one of #{@routes.keys.join(",")}" unless route_cache.has_key?(method)
25
+ route_cache[method] << Route.new(method, path, block)
26
+ end
48
27
  end
49
- end
50
28
 
51
- def match(env)
52
- request_method = env[REQUEST_METHOD]
53
- path_info = ::Rack::Utils.unescape env[PATH_INFO]
29
+ def match(env)
30
+ request_method = env["REQUEST_METHOD"].downcase.to_sym
31
+ path_info = ::Rack::Utils.unescape_path env["PATH_INFO"]
54
32
 
55
- # There are servers which send an empty string when requesting the root.
56
- # These servers should be ashamed of themselves.
57
- path_info = "/" if path_info == ""
33
+ # There are servers which send an empty string when requesting the root.
34
+ # These servers should be ashamed of themselves.
35
+ path_info = "/" if path_info == ""
58
36
 
59
- @routes[request_method].each do |route|
60
- params = route.match(request_method, path_info)
61
- if params
62
- env[ROUTE_PARAMS] = params
63
-
64
- return WebAction.new(env, route.block)
37
+ route_cache[request_method].each do |route|
38
+ params = route.match(request_method, path_info)
39
+ if params
40
+ env["rack.route_params"] = params
41
+ return Action.new(env, route.block)
42
+ end
65
43
  end
44
+
45
+ nil
66
46
  end
67
47
 
68
- nil
48
+ def route_cache
49
+ @@routes ||= {get: [], post: [], put: [], patch: [], delete: [], head: []}
50
+ end
69
51
  end
70
- end
71
52
 
72
- class WebRoute
73
- attr_accessor :request_method, :pattern, :block, :name
53
+ class Route
54
+ attr_accessor :request_method, :pattern, :block, :name
74
55
 
75
- NAMED_SEGMENTS_PATTERN = /\/([^\/]*):([^.:$\/]+)/
56
+ NAMED_SEGMENTS_PATTERN = /\/([^\/]*):([^.:$\/]+)/
76
57
 
77
- def initialize(request_method, pattern, block)
78
- @request_method = request_method
79
- @pattern = pattern
80
- @block = block
81
- end
58
+ def initialize(request_method, pattern, block)
59
+ @request_method = request_method
60
+ @pattern = pattern
61
+ @block = block
62
+ end
82
63
 
83
- def matcher
84
- @matcher ||= compile
85
- end
64
+ def matcher
65
+ @matcher ||= compile
66
+ end
86
67
 
87
- def compile
88
- if pattern.match?(NAMED_SEGMENTS_PATTERN)
89
- p = pattern.gsub(NAMED_SEGMENTS_PATTERN, '/\1(?<\2>[^$/]+)')
68
+ def compile
69
+ if pattern.match?(NAMED_SEGMENTS_PATTERN)
70
+ p = pattern.gsub(NAMED_SEGMENTS_PATTERN, '/\1(?<\2>[^$/]+)')
90
71
 
91
- Regexp.new("\\A#{p}\\Z")
92
- else
93
- pattern
72
+ Regexp.new("\\A#{p}\\Z")
73
+ else
74
+ pattern
75
+ end
94
76
  end
95
- end
96
77
 
97
- def match(request_method, path)
98
- case matcher
99
- when String
100
- {} if path == matcher
101
- else
102
- path_match = path.match(matcher)
103
- path_match&.named_captures&.transform_keys(&:to_sym)
78
+ EMPTY = {}.freeze
79
+
80
+ def match(request_method, path)
81
+ case matcher
82
+ when String
83
+ EMPTY if path == matcher
84
+ else
85
+ path_match = path.match(matcher)
86
+ path_match&.named_captures&.transform_keys(&:to_sym)
87
+ end
104
88
  end
105
89
  end
106
90
  end