sidekiq 4.1.4 → 4.2.10

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sidekiq might be problematic. Click here for more details.

Files changed (134) hide show
  1. checksums.yaml +4 -4
  2. data/.github/issue_template.md +6 -1
  3. data/.travis.yml +9 -9
  4. data/Changes.md +100 -0
  5. data/Ent-Changes.md +51 -1
  6. data/Gemfile +6 -6
  7. data/Pro-Changes.md +69 -0
  8. data/README.md +4 -3
  9. data/Rakefile +5 -2
  10. data/bin/sidekiqload +11 -24
  11. data/lib/generators/sidekiq/templates/worker_test.rb.erb +1 -1
  12. data/lib/sidekiq/api.rb +21 -13
  13. data/lib/sidekiq/cli.rb +19 -5
  14. data/lib/sidekiq/core_ext.rb +13 -0
  15. data/lib/sidekiq/launcher.rb +36 -23
  16. data/lib/sidekiq/manager.rb +3 -2
  17. data/lib/sidekiq/middleware/server/logging.rb +8 -17
  18. data/lib/sidekiq/middleware/server/retry_jobs.rb +1 -1
  19. data/lib/sidekiq/processor.rb +31 -16
  20. data/lib/sidekiq/rails.rb +84 -0
  21. data/lib/sidekiq/redis_connection.rb +8 -1
  22. data/lib/sidekiq/scheduled.rb +1 -0
  23. data/lib/sidekiq/testing.rb +10 -2
  24. data/lib/sidekiq/util.rb +2 -1
  25. data/lib/sidekiq/version.rb +1 -1
  26. data/lib/sidekiq/web/action.rb +93 -0
  27. data/lib/sidekiq/web/application.rb +336 -0
  28. data/lib/sidekiq/{web_helpers.rb → web/helpers.rb} +39 -16
  29. data/lib/sidekiq/web/router.rb +100 -0
  30. data/lib/sidekiq/web.rb +119 -184
  31. data/lib/sidekiq/worker.rb +3 -3
  32. data/lib/sidekiq.rb +7 -7
  33. data/sidekiq.gemspec +11 -5
  34. data/web/assets/images/{status-sd8051fd480.png → status.png} +0 -0
  35. data/web/assets/javascripts/application.js +24 -20
  36. data/web/assets/javascripts/dashboard.js +1 -1
  37. data/web/assets/stylesheets/application.css +26 -1
  38. data/web/assets/stylesheets/bootstrap.css +4 -8
  39. data/web/locales/de.yml +1 -1
  40. data/web/locales/fa.yml +79 -0
  41. data/web/views/_footer.erb +1 -1
  42. data/web/views/_job_info.erb +1 -1
  43. data/web/views/busy.erb +2 -2
  44. data/web/views/dashboard.erb +4 -4
  45. data/web/views/dead.erb +1 -1
  46. data/web/views/layout.erb +3 -4
  47. data/web/views/morgue.erb +14 -10
  48. data/web/views/queue.erb +6 -6
  49. data/web/views/queues.erb +3 -3
  50. data/web/views/retries.erb +12 -10
  51. data/web/views/retry.erb +2 -2
  52. data/web/views/scheduled.erb +2 -2
  53. data/web/views/scheduled_job_info.erb +1 -1
  54. metadata +86 -129
  55. data/test/config.yml +0 -9
  56. data/test/env_based_config.yml +0 -11
  57. data/test/fake_env.rb +0 -1
  58. data/test/fixtures/en.yml +0 -2
  59. data/test/helper.rb +0 -75
  60. data/test/test_actors.rb +0 -138
  61. data/test/test_api.rb +0 -528
  62. data/test/test_cli.rb +0 -406
  63. data/test/test_client.rb +0 -266
  64. data/test/test_exception_handler.rb +0 -56
  65. data/test/test_extensions.rb +0 -127
  66. data/test/test_fetch.rb +0 -50
  67. data/test/test_launcher.rb +0 -85
  68. data/test/test_logging.rb +0 -35
  69. data/test/test_manager.rb +0 -50
  70. data/test/test_middleware.rb +0 -158
  71. data/test/test_processor.rb +0 -201
  72. data/test/test_rails.rb +0 -22
  73. data/test/test_redis_connection.rb +0 -132
  74. data/test/test_retry.rb +0 -326
  75. data/test/test_retry_exhausted.rb +0 -149
  76. data/test/test_scheduled.rb +0 -115
  77. data/test/test_scheduling.rb +0 -50
  78. data/test/test_sidekiq.rb +0 -107
  79. data/test/test_testing.rb +0 -143
  80. data/test/test_testing_fake.rb +0 -357
  81. data/test/test_testing_inline.rb +0 -94
  82. data/test/test_util.rb +0 -13
  83. data/test/test_web.rb +0 -614
  84. data/test/test_web_helpers.rb +0 -54
  85. data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
  86. data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
  87. data/web/assets/images/status/active.png +0 -0
  88. data/web/assets/images/status/idle.png +0 -0
  89. data/web/assets/javascripts/locales/README.md +0 -27
  90. data/web/assets/javascripts/locales/jquery.timeago.ar.js +0 -96
  91. data/web/assets/javascripts/locales/jquery.timeago.bg.js +0 -18
  92. data/web/assets/javascripts/locales/jquery.timeago.bs.js +0 -49
  93. data/web/assets/javascripts/locales/jquery.timeago.ca.js +0 -18
  94. data/web/assets/javascripts/locales/jquery.timeago.cs.js +0 -18
  95. data/web/assets/javascripts/locales/jquery.timeago.cy.js +0 -20
  96. data/web/assets/javascripts/locales/jquery.timeago.da.js +0 -18
  97. data/web/assets/javascripts/locales/jquery.timeago.de.js +0 -18
  98. data/web/assets/javascripts/locales/jquery.timeago.el.js +0 -18
  99. data/web/assets/javascripts/locales/jquery.timeago.en-short.js +0 -20
  100. data/web/assets/javascripts/locales/jquery.timeago.en.js +0 -20
  101. data/web/assets/javascripts/locales/jquery.timeago.es.js +0 -18
  102. data/web/assets/javascripts/locales/jquery.timeago.et.js +0 -18
  103. data/web/assets/javascripts/locales/jquery.timeago.fa.js +0 -22
  104. data/web/assets/javascripts/locales/jquery.timeago.fi.js +0 -28
  105. data/web/assets/javascripts/locales/jquery.timeago.fr-short.js +0 -16
  106. data/web/assets/javascripts/locales/jquery.timeago.fr.js +0 -17
  107. data/web/assets/javascripts/locales/jquery.timeago.he.js +0 -18
  108. data/web/assets/javascripts/locales/jquery.timeago.hr.js +0 -49
  109. data/web/assets/javascripts/locales/jquery.timeago.hu.js +0 -18
  110. data/web/assets/javascripts/locales/jquery.timeago.hy.js +0 -18
  111. data/web/assets/javascripts/locales/jquery.timeago.id.js +0 -18
  112. data/web/assets/javascripts/locales/jquery.timeago.it.js +0 -16
  113. data/web/assets/javascripts/locales/jquery.timeago.ja.js +0 -19
  114. data/web/assets/javascripts/locales/jquery.timeago.ko.js +0 -17
  115. data/web/assets/javascripts/locales/jquery.timeago.lt.js +0 -20
  116. data/web/assets/javascripts/locales/jquery.timeago.mk.js +0 -20
  117. data/web/assets/javascripts/locales/jquery.timeago.nb.js +0 -18
  118. data/web/assets/javascripts/locales/jquery.timeago.nl.js +0 -20
  119. data/web/assets/javascripts/locales/jquery.timeago.pl.js +0 -31
  120. data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +0 -16
  121. data/web/assets/javascripts/locales/jquery.timeago.pt.js +0 -16
  122. data/web/assets/javascripts/locales/jquery.timeago.ro.js +0 -18
  123. data/web/assets/javascripts/locales/jquery.timeago.rs.js +0 -49
  124. data/web/assets/javascripts/locales/jquery.timeago.ru.js +0 -34
  125. data/web/assets/javascripts/locales/jquery.timeago.sk.js +0 -18
  126. data/web/assets/javascripts/locales/jquery.timeago.sl.js +0 -44
  127. data/web/assets/javascripts/locales/jquery.timeago.sv.js +0 -18
  128. data/web/assets/javascripts/locales/jquery.timeago.th.js +0 -20
  129. data/web/assets/javascripts/locales/jquery.timeago.tr.js +0 -16
  130. data/web/assets/javascripts/locales/jquery.timeago.uk.js +0 -34
  131. data/web/assets/javascripts/locales/jquery.timeago.uz.js +0 -19
  132. data/web/assets/javascripts/locales/jquery.timeago.zh-cn.js +0 -20
  133. data/web/assets/javascripts/locales/jquery.timeago.zh-tw.js +0 -20
  134. data/web/views/_poll_js.erb +0 -5
@@ -0,0 +1,336 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ class WebApplication
5
+ extend WebRouter
6
+
7
+ CONTENT_LENGTH = "Content-Length".freeze
8
+ CONTENT_TYPE = "Content-Type".freeze
9
+ REDIS_KEYS = %w(redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human)
10
+
11
+ def initialize(klass)
12
+ @klass = klass
13
+ end
14
+
15
+ def settings
16
+ @klass.settings
17
+ end
18
+
19
+ def self.settings
20
+ Sidekiq::Web.settings
21
+ end
22
+
23
+ def self.tabs
24
+ Sidekiq::Web.tabs
25
+ end
26
+
27
+ def self.set(key, val)
28
+ # nothing, backwards compatibility
29
+ end
30
+
31
+ get "/" do
32
+ @redis_info = redis_info.select{ |k, v| REDIS_KEYS.include? k }
33
+ stats_history = Sidekiq::Stats::History.new((params['days'] || 30).to_i)
34
+ @processed_history = stats_history.processed
35
+ @failed_history = stats_history.failed
36
+
37
+ erb(:dashboard)
38
+ end
39
+
40
+ get "/busy" do
41
+ erb(:busy)
42
+ end
43
+
44
+ post "/busy" do
45
+ if params['identity']
46
+ p = Sidekiq::Process.new('identity' => params['identity'])
47
+ p.quiet! if params['quiet']
48
+ p.stop! if params['stop']
49
+ else
50
+ processes.each do |pro|
51
+ pro.quiet! if params['quiet']
52
+ pro.stop! if params['stop']
53
+ end
54
+ end
55
+
56
+ redirect "#{root_path}busy"
57
+ end
58
+
59
+ get "/queues" do
60
+ @queues = Sidekiq::Queue.all
61
+
62
+ erb(:queues)
63
+ end
64
+
65
+ get "/queues/:name" do
66
+ @name = route_params[:name]
67
+
68
+ halt(404) unless @name
69
+
70
+ @count = (params['count'] || 25).to_i
71
+ @queue = Sidekiq::Queue.new(@name)
72
+ (@current_page, @total_size, @messages) = page("queue:#{@name}", params['page'], @count)
73
+ @messages = @messages.map { |msg| Sidekiq::Job.new(msg, @name) }
74
+
75
+ erb(:queue)
76
+ end
77
+
78
+ post "/queues/:name" do
79
+ Sidekiq::Queue.new(route_params[:name]).clear
80
+
81
+ redirect "#{root_path}queues"
82
+ end
83
+
84
+ post "/queues/:name/delete" do
85
+ name = route_params[:name]
86
+ Sidekiq::Job.new(params['key_val'], name).delete
87
+
88
+ redirect_with_query("#{root_path}queues/#{CGI.escape(name)}")
89
+ end
90
+
91
+ get '/morgue' do
92
+ @count = (params['count'] || 25).to_i
93
+ (@current_page, @total_size, @dead) = page("dead", params['page'], @count, reverse: true)
94
+ @dead = @dead.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
95
+
96
+ erb(:morgue)
97
+ end
98
+
99
+ get "/morgue/:key" do
100
+ halt(404) unless key = route_params[:key]
101
+
102
+ @dead = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
103
+
104
+ if @dead.nil?
105
+ redirect "#{root_path}morgue"
106
+ else
107
+ erb(:dead)
108
+ end
109
+ end
110
+
111
+ post '/morgue' do
112
+ redirect(request.path) unless params['key']
113
+
114
+ params['key'].each do |key|
115
+ job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
116
+ retry_or_delete_or_kill job, params if job
117
+ end
118
+
119
+ redirect_with_query("#{root_path}morgue")
120
+ end
121
+
122
+ post "/morgue/all/delete" do
123
+ Sidekiq::DeadSet.new.clear
124
+
125
+ redirect "#{root_path}morgue"
126
+ end
127
+
128
+ post "/morgue/all/retry" do
129
+ Sidekiq::DeadSet.new.retry_all
130
+
131
+ redirect "#{root_path}morgue"
132
+ end
133
+
134
+ post "/morgue/:key" do
135
+ halt(404) unless key = route_params[:key]
136
+
137
+ job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
138
+ retry_or_delete_or_kill job, params if job
139
+
140
+ redirect_with_query("#{root_path}morgue")
141
+ end
142
+
143
+ get '/retries' do
144
+ @count = (params['count'] || 25).to_i
145
+ (@current_page, @total_size, @retries) = page("retry", params['page'], @count)
146
+ @retries = @retries.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
147
+
148
+ erb(:retries)
149
+ end
150
+
151
+ get "/retries/:key" do
152
+ @retry = Sidekiq::RetrySet.new.fetch(*parse_params(route_params[:key])).first
153
+
154
+ if @retry.nil?
155
+ redirect "#{root_path}retries"
156
+ else
157
+ erb(:retry)
158
+ end
159
+ end
160
+
161
+ post '/retries' do
162
+ redirect(request.path) unless params['key']
163
+
164
+ params['key'].each do |key|
165
+ job = Sidekiq::RetrySet.new.fetch(*parse_params(key)).first
166
+ retry_or_delete_or_kill job, params if job
167
+ end
168
+
169
+ redirect_with_query("#{root_path}retries")
170
+ end
171
+
172
+ post "/retries/all/delete" do
173
+ Sidekiq::RetrySet.new.clear
174
+
175
+ redirect "#{root_path}retries"
176
+ end
177
+
178
+ post "/retries/all/retry" do
179
+ Sidekiq::RetrySet.new.retry_all
180
+
181
+ redirect "#{root_path}retries"
182
+ end
183
+
184
+ post "/retries/:key" do
185
+ job = Sidekiq::RetrySet.new.fetch(*parse_params(route_params[:key])).first
186
+
187
+ retry_or_delete_or_kill job, params if job
188
+
189
+ redirect_with_query("#{root_path}retries")
190
+ end
191
+
192
+ get '/scheduled' do
193
+ @count = (params['count'] || 25).to_i
194
+ (@current_page, @total_size, @scheduled) = page("schedule", params['page'], @count)
195
+ @scheduled = @scheduled.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
196
+
197
+ erb(:scheduled)
198
+ end
199
+
200
+ get "/scheduled/:key" do
201
+ @job = Sidekiq::ScheduledSet.new.fetch(*parse_params(route_params[:key])).first
202
+
203
+ if @job.nil?
204
+ redirect "#{root_path}scheduled"
205
+ else
206
+ erb(:scheduled_job_info)
207
+ end
208
+ end
209
+
210
+ post '/scheduled' do
211
+ redirect(request.path) unless params['key']
212
+
213
+ params['key'].each do |key|
214
+ job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
215
+ delete_or_add_queue job, params if job
216
+ end
217
+
218
+ redirect_with_query("#{root_path}scheduled")
219
+ end
220
+
221
+ post "/scheduled/:key" do
222
+ halt(404) unless key = route_params[:key]
223
+
224
+ job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
225
+ delete_or_add_queue job, params if job
226
+
227
+ redirect_with_query("#{root_path}scheduled")
228
+ end
229
+
230
+ get '/dashboard/stats' do
231
+ redirect "#{root_path}stats"
232
+ end
233
+
234
+ get '/stats' do
235
+ sidekiq_stats = Sidekiq::Stats.new
236
+ redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
237
+
238
+ json(
239
+ sidekiq: {
240
+ processed: sidekiq_stats.processed,
241
+ failed: sidekiq_stats.failed,
242
+ busy: sidekiq_stats.workers_size,
243
+ processes: sidekiq_stats.processes_size,
244
+ enqueued: sidekiq_stats.enqueued,
245
+ scheduled: sidekiq_stats.scheduled_size,
246
+ retries: sidekiq_stats.retry_size,
247
+ dead: sidekiq_stats.dead_size,
248
+ default_latency: sidekiq_stats.default_queue_latency
249
+ },
250
+ redis: redis_stats
251
+ )
252
+ end
253
+
254
+ get '/stats/queues' do
255
+ json Sidekiq::Stats::Queues.new.lengths
256
+ end
257
+
258
+ def call(env)
259
+ action = self.class.match(env)
260
+ return [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass" }, ["Not Found"]] unless action
261
+
262
+ resp = catch(:halt) do
263
+ app = @klass
264
+ self.class.run_befores(app, action)
265
+ begin
266
+ resp = action.instance_exec env, &action.block
267
+ ensure
268
+ self.class.run_afters(app, action)
269
+ end
270
+
271
+ resp
272
+ end
273
+
274
+ resp = case resp
275
+ when Array
276
+ resp
277
+ when Integer
278
+ [resp, {}, []]
279
+ else
280
+ type_header = case action.type
281
+ when :json
282
+ { "Content-Type" => "application/json", "Cache-Control" => "no-cache" }
283
+ when String
284
+ { "Content-Type" => (action.type || "text/html"), "Cache-Control" => "no-cache" }
285
+ else
286
+ { "Content-Type" => "text/html", "Cache-Control" => "no-cache" }
287
+ end
288
+
289
+ [200, type_header, [resp]]
290
+ end
291
+
292
+ resp[1] = resp[1].dup
293
+
294
+ resp[1][CONTENT_LENGTH] = resp[2].inject(0) { |l, p| l + p.bytesize }.to_s
295
+
296
+ resp
297
+ end
298
+
299
+ def self.helpers(mod=nil, &block)
300
+ if block_given?
301
+ WebAction.class_eval(&block)
302
+ else
303
+ WebAction.send(:include, mod)
304
+ end
305
+ end
306
+
307
+ def self.before(path=nil, &block)
308
+ befores << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
309
+ end
310
+
311
+ def self.after(path=nil, &block)
312
+ afters << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
313
+ end
314
+
315
+ def self.run_befores(app, action)
316
+ run_hooks(befores, app, action)
317
+ end
318
+
319
+ def self.run_afters(app, action)
320
+ run_hooks(afters, app, action)
321
+ end
322
+
323
+ def self.run_hooks(hooks, app, action)
324
+ hooks.select { |p,_| !p || p =~ action.env[WebRouter::PATH_INFO] }.
325
+ each {|_,b| action.instance_exec(action.env, app, &b) }
326
+ end
327
+
328
+ def self.befores
329
+ @befores ||= []
330
+ end
331
+
332
+ def self.afters
333
+ @afters ||= []
334
+ end
335
+ end
336
+ end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
  require 'uri'
3
+ require 'set'
4
+ require 'yaml'
5
+ require 'cgi'
3
6
 
4
7
  module Sidekiq
5
8
  # This is not a public API
@@ -45,21 +48,21 @@ module Sidekiq
45
48
  # <meta .../>
46
49
  # <% end %>
47
50
  #
48
- def add_to_head(&block)
51
+ def add_to_head
49
52
  @head_html ||= []
50
- @head_html << block if block_given?
53
+ @head_html << yield.dup if block_given?
51
54
  end
52
55
 
53
56
  def display_custom_head
54
- return unless defined?(@head_html)
55
- @head_html.map { |block| capture(&block) }.join
57
+ @head_html.join if defined?(@head_html)
56
58
  end
57
59
 
58
- # Simple capture method for erb templates. The origin was
59
- # capture method from sinatra-contrib library.
60
- def capture(&block)
61
- block.call
62
- eval('', block.binding)
60
+ def poll_path
61
+ if current_path != '' && params['poll']
62
+ root_path + current_path
63
+ else
64
+ ""
65
+ end
63
66
  end
64
67
 
65
68
  # Given a browser request Accept-Language header like
@@ -69,7 +72,7 @@ module Sidekiq
69
72
  def locale
70
73
  @locale ||= begin
71
74
  locale = 'en'.freeze
72
- languages = request.env['HTTP_ACCEPT_LANGUAGE'.freeze] || 'en'.freeze
75
+ languages = env['HTTP_ACCEPT_LANGUAGE'.freeze] || 'en'.freeze
73
76
  languages.downcase.split(','.freeze).each do |lang|
74
77
  next if lang == '*'.freeze
75
78
  lang = lang.split(';'.freeze)[0]
@@ -79,6 +82,11 @@ module Sidekiq
79
82
  end
80
83
  end
81
84
 
85
+ # mperham/sidekiq#3243
86
+ def unfiltered?
87
+ yield unless env['PATH_INFO'].start_with?("/filter/")
88
+ end
89
+
82
90
  def get_locale
83
91
  strings(locale)
84
92
  end
@@ -110,10 +118,6 @@ module Sidekiq
110
118
  end.map { |msg| Sidekiq.load_json(msg) }
111
119
  end
112
120
 
113
- def location
114
- Sidekiq.redis { |conn| conn.client.location }
115
- end
116
-
117
121
  def redis_connection
118
122
  Sidekiq.redis { |conn| conn.client.id }
119
123
  end
@@ -139,7 +143,8 @@ module Sidekiq
139
143
  end
140
144
 
141
145
  def relative_time(time)
142
- %{<time datetime="#{time.getutc.iso8601}">#{time}</time>}
146
+ stamp = time.getutc.iso8601
147
+ %{<time title="#{stamp}" datetime="#{stamp}">#{time}</time>}
143
148
  end
144
149
 
145
150
  def job_params(job, score)
@@ -157,7 +162,7 @@ module Sidekiq
157
162
  def qparams(options)
158
163
  options = options.stringify_keys
159
164
  params.merge(options).map do |key, value|
160
- SAFE_QPARAMS.include?(key) ? "#{key}=#{value}" : next
165
+ SAFE_QPARAMS.include?(key) ? "#{key}=#{CGI.escape(value.to_s)}" : next
161
166
  end.compact.join("&")
162
167
  end
163
168
 
@@ -251,5 +256,23 @@ module Sidekiq
251
256
  "#{redis_connection}#{namespace_suffix}"
252
257
  end
253
258
  end
259
+
260
+ def retry_or_delete_or_kill(job, params)
261
+ if params['retry']
262
+ job.retry
263
+ elsif params['delete']
264
+ job.delete
265
+ elsif params['kill']
266
+ job.kill
267
+ end
268
+ end
269
+
270
+ def delete_or_add_queue(job, params)
271
+ if params['delete']
272
+ job.delete
273
+ elsif params['add_to_queue']
274
+ job.add_to_queue
275
+ end
276
+ end
254
277
  end
255
278
  end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+ require 'rack'
3
+
4
+ module Sidekiq
5
+ module WebRouter
6
+ GET = 'GET'.freeze
7
+ DELETE = 'DELETE'.freeze
8
+ POST = 'POST'.freeze
9
+ PUT = 'PUT'.freeze
10
+ PATCH = 'PATCH'.freeze
11
+ HEAD = 'HEAD'.freeze
12
+
13
+ ROUTE_PARAMS = 'rack.route_params'.freeze
14
+ REQUEST_METHOD = 'REQUEST_METHOD'.freeze
15
+ PATH_INFO = 'PATH_INFO'.freeze
16
+
17
+ def get(path, &block)
18
+ route(GET, path, &block)
19
+ end
20
+
21
+ def post(path, &block)
22
+ route(POST, path, &block)
23
+ end
24
+
25
+ def put(path, &block)
26
+ route(PUT, path, &block)
27
+ end
28
+
29
+ def patch(path, &block)
30
+ route(PATCH, path, &block)
31
+ end
32
+
33
+ def delete(path, &block)
34
+ route(DELETE, path, &block)
35
+ end
36
+
37
+ def route(method, path, &block)
38
+ @routes ||= { GET => [], POST => [], PUT => [], PATCH => [], DELETE => [], HEAD => [] }
39
+
40
+ @routes[method] << WebRoute.new(method, path, block)
41
+ @routes[HEAD] << WebRoute.new(method, path, block) if method == GET
42
+ end
43
+
44
+ def match(env)
45
+ request_method = env[REQUEST_METHOD]
46
+ path_info = ::Rack::Utils.unescape env[PATH_INFO]
47
+
48
+ # There are servers which send an empty string when requesting the root.
49
+ # These servers should be ashamed of themselves.
50
+ path_info = "/" if path_info == ""
51
+
52
+ @routes[request_method].each do |route|
53
+ if params = route.match(request_method, path_info)
54
+ env[ROUTE_PARAMS] = params
55
+
56
+ return WebAction.new(env, route.block)
57
+ end
58
+ end
59
+
60
+ nil
61
+ end
62
+ end
63
+
64
+ class WebRoute
65
+ attr_accessor :request_method, :pattern, :block, :name
66
+
67
+ NAMED_SEGMENTS_PATTERN = /\/([^\/]*):([^\.:$\/]+)/.freeze
68
+
69
+ def initialize(request_method, pattern, block)
70
+ @request_method = request_method
71
+ @pattern = pattern
72
+ @block = block
73
+ end
74
+
75
+ def matcher
76
+ @matcher ||= compile
77
+ end
78
+
79
+ def compile
80
+ if pattern.match(NAMED_SEGMENTS_PATTERN)
81
+ p = pattern.gsub(NAMED_SEGMENTS_PATTERN, '/\1(?<\2>[^$/]+)')
82
+
83
+ Regexp.new("\\A#{p}\\Z")
84
+ else
85
+ pattern
86
+ end
87
+ end
88
+
89
+ def match(request_method, path)
90
+ case matcher
91
+ when String
92
+ {} if path == matcher
93
+ else
94
+ if path_match = path.match(matcher)
95
+ Hash[path_match.names.map(&:to_sym).zip(path_match.captures)]
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end