ed-precompiled_puma 7.0.4

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 (88) hide show
  1. checksums.yaml +7 -0
  2. data/History.md +3172 -0
  3. data/LICENSE +29 -0
  4. data/README.md +477 -0
  5. data/bin/puma +10 -0
  6. data/bin/puma-wild +25 -0
  7. data/bin/pumactl +12 -0
  8. data/docs/architecture.md +74 -0
  9. data/docs/compile_options.md +55 -0
  10. data/docs/deployment.md +102 -0
  11. data/docs/fork_worker.md +41 -0
  12. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  13. data/docs/images/puma-connection-flow.png +0 -0
  14. data/docs/images/puma-general-arch.png +0 -0
  15. data/docs/java_options.md +54 -0
  16. data/docs/jungle/README.md +9 -0
  17. data/docs/jungle/rc.d/README.md +74 -0
  18. data/docs/jungle/rc.d/puma +61 -0
  19. data/docs/jungle/rc.d/puma.conf +10 -0
  20. data/docs/kubernetes.md +80 -0
  21. data/docs/nginx.md +80 -0
  22. data/docs/plugins.md +42 -0
  23. data/docs/rails_dev_mode.md +28 -0
  24. data/docs/restart.md +65 -0
  25. data/docs/signals.md +98 -0
  26. data/docs/stats.md +148 -0
  27. data/docs/systemd.md +253 -0
  28. data/docs/testing_benchmarks_local_files.md +150 -0
  29. data/docs/testing_test_rackup_ci_files.md +36 -0
  30. data/ext/puma_http11/PumaHttp11Service.java +17 -0
  31. data/ext/puma_http11/ext_help.h +15 -0
  32. data/ext/puma_http11/extconf.rb +65 -0
  33. data/ext/puma_http11/http11_parser.c +1057 -0
  34. data/ext/puma_http11/http11_parser.h +65 -0
  35. data/ext/puma_http11/http11_parser.java.rl +145 -0
  36. data/ext/puma_http11/http11_parser.rl +149 -0
  37. data/ext/puma_http11/http11_parser_common.rl +54 -0
  38. data/ext/puma_http11/mini_ssl.c +852 -0
  39. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  40. data/ext/puma_http11/org/jruby/puma/Http11.java +257 -0
  41. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +455 -0
  42. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +509 -0
  43. data/ext/puma_http11/puma_http11.c +507 -0
  44. data/lib/puma/app/status.rb +96 -0
  45. data/lib/puma/binder.rb +511 -0
  46. data/lib/puma/cli.rb +245 -0
  47. data/lib/puma/client.rb +720 -0
  48. data/lib/puma/cluster/worker.rb +182 -0
  49. data/lib/puma/cluster/worker_handle.rb +127 -0
  50. data/lib/puma/cluster.rb +635 -0
  51. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  52. data/lib/puma/commonlogger.rb +115 -0
  53. data/lib/puma/configuration.rb +452 -0
  54. data/lib/puma/const.rb +307 -0
  55. data/lib/puma/control_cli.rb +320 -0
  56. data/lib/puma/detect.rb +47 -0
  57. data/lib/puma/dsl.rb +1480 -0
  58. data/lib/puma/error_logger.rb +115 -0
  59. data/lib/puma/events.rb +72 -0
  60. data/lib/puma/io_buffer.rb +50 -0
  61. data/lib/puma/jruby_restart.rb +11 -0
  62. data/lib/puma/json_serialization.rb +96 -0
  63. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  64. data/lib/puma/launcher.rb +496 -0
  65. data/lib/puma/log_writer.rb +147 -0
  66. data/lib/puma/minissl/context_builder.rb +96 -0
  67. data/lib/puma/minissl.rb +463 -0
  68. data/lib/puma/null_io.rb +101 -0
  69. data/lib/puma/plugin/systemd.rb +90 -0
  70. data/lib/puma/plugin/tmp_restart.rb +36 -0
  71. data/lib/puma/plugin.rb +111 -0
  72. data/lib/puma/rack/builder.rb +297 -0
  73. data/lib/puma/rack/urlmap.rb +93 -0
  74. data/lib/puma/rack_default.rb +24 -0
  75. data/lib/puma/reactor.rb +140 -0
  76. data/lib/puma/request.rb +701 -0
  77. data/lib/puma/runner.rb +211 -0
  78. data/lib/puma/sd_notify.rb +146 -0
  79. data/lib/puma/server.rb +734 -0
  80. data/lib/puma/single.rb +72 -0
  81. data/lib/puma/state_file.rb +69 -0
  82. data/lib/puma/thread_pool.rb +402 -0
  83. data/lib/puma/util.rb +134 -0
  84. data/lib/puma.rb +93 -0
  85. data/lib/rack/handler/puma.rb +144 -0
  86. data/tools/Dockerfile +18 -0
  87. data/tools/trickletest.rb +44 -0
  88. metadata +152 -0
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'runner'
4
+ require_relative 'detect'
5
+ require_relative 'plugin'
6
+
7
+ module Puma
8
+ # This class is instantiated by the `Puma::Launcher` and used
9
+ # to boot and serve a Ruby application when no puma "workers" are needed
10
+ # i.e. only using "threaded" mode. For example `$ puma -t 1:5`
11
+ #
12
+ # At the core of this class is running an instance of `Puma::Server` which
13
+ # gets created via the `start_server` method from the `Puma::Runner` class
14
+ # that this inherits from.
15
+ class Single < Runner
16
+ # @!attribute [r] stats
17
+ def stats
18
+ {
19
+ started_at: utc_iso8601(@started_at)
20
+ }.merge(@server&.stats || {}).merge(super)
21
+ end
22
+
23
+ def restart
24
+ @server&.begin_restart
25
+ end
26
+
27
+ def stop
28
+ @server&.stop false
29
+ end
30
+
31
+ def halt
32
+ @server&.halt
33
+ end
34
+
35
+ def stop_blocked
36
+ log "- Gracefully stopping, waiting for requests to finish"
37
+ @control&.stop true
38
+ @server&.stop true
39
+ end
40
+
41
+ def run
42
+ output_header "single"
43
+
44
+ load_and_bind
45
+
46
+ Plugins.fire_background
47
+
48
+ @launcher.write_state
49
+
50
+ start_control
51
+
52
+ @server = server = start_server
53
+ server_thread = server.run
54
+
55
+ log "Use Ctrl-C to stop"
56
+
57
+ warn_ruby_mn_threads
58
+
59
+ redirect_io
60
+
61
+ @events.fire_after_booted!
62
+
63
+ debug_loaded_extensions("Loaded Extensions:") if @log_writer.debug?
64
+
65
+ begin
66
+ server_thread.join
67
+ rescue Interrupt
68
+ # Swallow it
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puma
4
+
5
+ # Puma::Launcher uses StateFile to write a yaml file for use with Puma::ControlCLI.
6
+ #
7
+ # In previous versions of Puma, YAML was used to read/write the state file.
8
+ # Since Puma is similar to Bundler/RubyGems in that it may load before one's app
9
+ # does, minimizing the dependencies that may be shared with the app is desired.
10
+ #
11
+ # At present, it only works with numeric and string values. It is still a valid
12
+ # yaml file, and the CI tests parse it with Psych.
13
+ #
14
+ class StateFile
15
+
16
+ ALLOWED_FIELDS = %w!control_url control_auth_token pid running_from!
17
+
18
+ def initialize
19
+ @options = {}
20
+ end
21
+
22
+ def save(path, permission = nil)
23
+ contents = +"---\n"
24
+ @options.each do |k,v|
25
+ next unless ALLOWED_FIELDS.include? k
26
+ case v
27
+ when Numeric
28
+ contents << "#{k}: #{v}\n"
29
+ when String
30
+ next if v.strip.empty?
31
+ contents << (k == 'running_from' || v.to_s.include?(' ') ?
32
+ "#{k}: \"#{v}\"\n" : "#{k}: #{v}\n")
33
+ end
34
+ end
35
+
36
+ if permission
37
+ File.write path, contents, mode: 'wb:UTF-8', perm: permission
38
+ else
39
+ File.write path, contents, mode: 'wb:UTF-8'
40
+ end
41
+ end
42
+
43
+ def load(path)
44
+ File.read(path).lines.each do |line|
45
+ next if line.start_with? '#'
46
+ k,v = line.split ':', 2
47
+ next unless v && ALLOWED_FIELDS.include?(k)
48
+ v = v.strip
49
+ @options[k] =
50
+ case v
51
+ when '' then nil
52
+ when /\A\d+\z/ then v.to_i
53
+ when /\A\d+\.\d+\z/ then v.to_f
54
+ else v.gsub(/\A"|"\z/, '')
55
+ end
56
+ end
57
+ end
58
+
59
+ ALLOWED_FIELDS.each do |f|
60
+ define_method f.to_sym do
61
+ @options[f]
62
+ end
63
+
64
+ define_method :"#{f}=" do |v|
65
+ @options[f] = v
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,402 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thread'
4
+
5
+ require_relative 'io_buffer'
6
+
7
+ module Puma
8
+ # Internal Docs for A simple thread pool management object.
9
+ #
10
+ # Each Puma "worker" has a thread pool to process requests.
11
+ #
12
+ # First a connection to a client is made in `Puma::Server`. It is wrapped in a
13
+ # `Puma::Client` instance and then passed to the `Puma::Reactor` to ensure
14
+ # the whole request is buffered into memory. Once the request is ready, it is passed into
15
+ # a thread pool via the `Puma::ThreadPool#<<` operator where it is stored in a `@todo` array.
16
+ #
17
+ # Each thread in the pool has an internal loop where it pulls a request from the `@todo` array
18
+ # and processes it.
19
+ class ThreadPool
20
+ class ForceShutdown < RuntimeError
21
+ end
22
+
23
+ # How long, after raising the ForceShutdown of a thread during
24
+ # forced shutdown mode, to wait for the thread to try and finish
25
+ # up its work before leaving the thread to die on the vine.
26
+ SHUTDOWN_GRACE_TIME = 5 # seconds
27
+
28
+ attr_reader :out_of_band_running
29
+
30
+ # Maintain a minimum of +min+ and maximum of +max+ threads
31
+ # in the pool.
32
+ #
33
+ # The block passed is the work that will be performed in each
34
+ # thread.
35
+ #
36
+ def initialize(name, options = {}, &block)
37
+ @not_empty = ConditionVariable.new
38
+ @not_full = ConditionVariable.new
39
+ @mutex = Mutex.new
40
+ @todo = Queue.new
41
+
42
+ @backlog_max = 0
43
+ @spawned = 0
44
+ @waiting = 0
45
+
46
+ @name = name
47
+ @min = Integer(options[:min_threads])
48
+ @max = Integer(options[:max_threads])
49
+ # Not an 'exposed' option, options[:pool_shutdown_grace_time] is used in CI
50
+ # to shorten @shutdown_grace_time from SHUTDOWN_GRACE_TIME. Parallel CI
51
+ # makes stubbing constants difficult.
52
+ @shutdown_grace_time = Float(options[:pool_shutdown_grace_time] || SHUTDOWN_GRACE_TIME)
53
+ @block = block
54
+ @out_of_band = options[:out_of_band]
55
+ @out_of_band_running = false
56
+ @out_of_band_condvar = ConditionVariable.new
57
+ @before_thread_start = options[:before_thread_start]
58
+ @before_thread_exit = options[:before_thread_exit]
59
+ @reaping_time = options[:reaping_time]
60
+ @auto_trim_time = options[:auto_trim_time]
61
+
62
+ @shutdown = false
63
+
64
+ @trim_requested = 0
65
+ @out_of_band_pending = false
66
+
67
+ @workers = []
68
+
69
+ @auto_trim = nil
70
+ @reaper = nil
71
+
72
+ @mutex.synchronize do
73
+ @min.times do
74
+ spawn_thread
75
+ @not_full.wait(@mutex)
76
+ end
77
+ end
78
+
79
+ @force_shutdown = false
80
+ @shutdown_mutex = Mutex.new
81
+ end
82
+
83
+ attr_reader :spawned, :trim_requested, :waiting
84
+
85
+ # generate stats hash so as not to perform multiple locks
86
+ # @return [Hash] hash containing stat info from ThreadPool
87
+ def stats
88
+ with_mutex do
89
+ temp = @backlog_max
90
+ @backlog_max = 0
91
+ { backlog: @todo.size,
92
+ running: @spawned,
93
+ pool_capacity: @waiting + (@max - @spawned),
94
+ busy_threads: @spawned - @waiting + @todo.size,
95
+ backlog_max: temp
96
+ }
97
+ end
98
+ end
99
+
100
+ def reset_max
101
+ with_mutex { @backlog_max = 0 }
102
+ end
103
+
104
+ # How many objects have yet to be processed by the pool?
105
+ #
106
+ def backlog
107
+ with_mutex { @todo.size }
108
+ end
109
+
110
+ # The maximum size of the backlog
111
+ #
112
+ def backlog_max
113
+ with_mutex { @backlog_max }
114
+ end
115
+
116
+ # @!attribute [r] pool_capacity
117
+ def pool_capacity
118
+ waiting + (@max - spawned)
119
+ end
120
+
121
+ # @!attribute [r] busy_threads
122
+ # @version 5.0.0
123
+ def busy_threads
124
+ with_mutex { @spawned - @waiting + @todo.size }
125
+ end
126
+
127
+ # :nodoc:
128
+ #
129
+ # Must be called with @mutex held!
130
+ #
131
+ def spawn_thread
132
+ @spawned += 1
133
+
134
+ trigger_before_thread_start_hooks
135
+ th = Thread.new(@spawned) do |spawned|
136
+ Puma.set_thread_name '%s tp %03i' % [@name, spawned]
137
+ todo = @todo
138
+ block = @block
139
+ mutex = @mutex
140
+ not_empty = @not_empty
141
+ not_full = @not_full
142
+
143
+ while true
144
+ work = nil
145
+
146
+ mutex.synchronize do
147
+ while todo.empty?
148
+ if @trim_requested > 0
149
+ @trim_requested -= 1
150
+ @spawned -= 1
151
+ @workers.delete th
152
+ not_full.signal
153
+ trigger_before_thread_exit_hooks
154
+ Thread.exit
155
+ end
156
+
157
+ @waiting += 1
158
+ if @out_of_band_pending && trigger_out_of_band_hook
159
+ @out_of_band_pending = false
160
+ end
161
+ not_full.signal
162
+ begin
163
+ not_empty.wait mutex
164
+ ensure
165
+ @waiting -= 1
166
+ end
167
+ end
168
+
169
+ work = todo.shift
170
+ end
171
+
172
+ begin
173
+ @out_of_band_pending = true if block.call(work)
174
+ rescue Exception => e
175
+ STDERR.puts "Error reached top of thread-pool: #{e.message} (#{e.class})"
176
+ end
177
+ end
178
+ end
179
+
180
+ @workers << th
181
+
182
+ th
183
+ end
184
+
185
+ private :spawn_thread
186
+
187
+ def trigger_before_thread_start_hooks
188
+ return unless @before_thread_start&.any?
189
+
190
+ @before_thread_start.each do |b|
191
+ begin
192
+ b[:block].call
193
+ rescue Exception => e
194
+ STDERR.puts "WARNING before_thread_start hook failed with exception (#{e.class}) #{e.message}"
195
+ end
196
+ end
197
+ nil
198
+ end
199
+
200
+ private :trigger_before_thread_start_hooks
201
+
202
+ def trigger_before_thread_exit_hooks
203
+ return unless @before_thread_exit&.any?
204
+
205
+ @before_thread_exit.each do |b|
206
+ begin
207
+ b[:block].call
208
+ rescue Exception => e
209
+ STDERR.puts "WARNING before_thread_exit hook failed with exception (#{e.class}) #{e.message}"
210
+ end
211
+ end
212
+ nil
213
+ end
214
+
215
+ private :trigger_before_thread_exit_hooks
216
+
217
+ # @version 5.0.0
218
+ def trigger_out_of_band_hook
219
+ return false unless @out_of_band&.any?
220
+
221
+ # we execute on idle hook when all threads are free
222
+ return false unless @spawned == @waiting
223
+ @out_of_band_running = true
224
+ @out_of_band.each { |b| b[:block].call }
225
+ true
226
+ rescue Exception => e
227
+ STDERR.puts "Exception calling out_of_band_hook: #{e.message} (#{e.class})"
228
+ true
229
+ ensure
230
+ @out_of_band_running = false
231
+ @out_of_band_condvar.broadcast
232
+ end
233
+
234
+ private :trigger_out_of_band_hook
235
+
236
+ def wait_while_out_of_band_running
237
+ return unless @out_of_band_running
238
+
239
+ with_mutex do
240
+ @out_of_band_condvar.wait(@mutex) while @out_of_band_running
241
+ end
242
+ end
243
+
244
+ # @version 5.0.0
245
+ def with_mutex(&block)
246
+ @mutex.owned? ?
247
+ yield :
248
+ @mutex.synchronize(&block)
249
+ end
250
+
251
+ # Add +work+ to the todo list for a Thread to pickup and process.
252
+ def <<(work)
253
+ with_mutex do
254
+ if @shutdown
255
+ raise "Unable to add work while shutting down"
256
+ end
257
+
258
+ @todo << work
259
+ t = @todo.size
260
+ @backlog_max = t if t > @backlog_max
261
+
262
+ if @waiting < @todo.size and @spawned < @max
263
+ spawn_thread
264
+ end
265
+
266
+ @not_empty.signal
267
+ end
268
+ end
269
+
270
+ # If there are any free threads in the pool, tell one to go ahead
271
+ # and exit. If +force+ is true, then a trim request is requested
272
+ # even if all threads are being utilized.
273
+ #
274
+ def trim(force=false)
275
+ with_mutex do
276
+ free = @waiting - @todo.size
277
+ if (force or free > 0) and @spawned - @trim_requested > @min
278
+ @trim_requested += 1
279
+ @not_empty.signal
280
+ end
281
+ end
282
+ end
283
+
284
+ # If there are dead threads in the pool make them go away while decreasing
285
+ # spawned counter so that new healthy threads could be created again.
286
+ def reap
287
+ with_mutex do
288
+ dead_workers = @workers.reject(&:alive?)
289
+
290
+ dead_workers.each do |worker|
291
+ worker.kill
292
+ @spawned -= 1
293
+ end
294
+
295
+ @workers.delete_if do |w|
296
+ dead_workers.include?(w)
297
+ end
298
+ end
299
+ end
300
+
301
+ class Automaton
302
+ def initialize(pool, timeout, thread_name, message)
303
+ @pool = pool
304
+ @timeout = timeout
305
+ @thread_name = thread_name
306
+ @message = message
307
+ @running = false
308
+ end
309
+
310
+ def start!
311
+ @running = true
312
+
313
+ @thread = Thread.new do
314
+ Puma.set_thread_name @thread_name
315
+ while @running
316
+ @pool.public_send(@message)
317
+ sleep @timeout
318
+ end
319
+ end
320
+ end
321
+
322
+ def stop
323
+ @running = false
324
+ @thread.wakeup
325
+ end
326
+ end
327
+
328
+ def auto_trim!(timeout=@auto_trim_time)
329
+ @auto_trim = Automaton.new(self, timeout, "#{@name} tp trim", :trim)
330
+ @auto_trim.start!
331
+ end
332
+
333
+ def auto_reap!(timeout=@reaping_time)
334
+ @reaper = Automaton.new(self, timeout, "#{@name} tp reap", :reap)
335
+ @reaper.start!
336
+ end
337
+
338
+ # Allows ThreadPool::ForceShutdown to be raised within the
339
+ # provided block if the thread is forced to shutdown during execution.
340
+ def with_force_shutdown
341
+ t = Thread.current
342
+ @shutdown_mutex.synchronize do
343
+ raise ForceShutdown if @force_shutdown
344
+ t[:with_force_shutdown] = true
345
+ end
346
+ yield
347
+ ensure
348
+ t[:with_force_shutdown] = false
349
+ end
350
+
351
+ # Tell all threads in the pool to exit and wait for them to finish.
352
+ # Wait +timeout+ seconds then raise +ForceShutdown+ in remaining threads.
353
+ # Next, wait an extra +@shutdown_grace_time+ seconds then force-kill remaining
354
+ # threads. Finally, wait 1 second for remaining threads to exit.
355
+ #
356
+ def shutdown(timeout=-1)
357
+ threads = with_mutex do
358
+ @shutdown = true
359
+ @trim_requested = @spawned
360
+ @not_empty.broadcast
361
+ @not_full.broadcast
362
+
363
+ @auto_trim&.stop
364
+ @reaper&.stop
365
+ # dup workers so that we join them all safely
366
+ @workers.dup
367
+ end
368
+
369
+ if timeout == -1
370
+ # Wait for threads to finish without force shutdown.
371
+ threads.each(&:join)
372
+ else
373
+ join = ->(inner_timeout) do
374
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
375
+ threads.reject! do |t|
376
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
377
+ t.join inner_timeout - elapsed
378
+ end
379
+ end
380
+
381
+ # Wait +timeout+ seconds for threads to finish.
382
+ join.call(timeout)
383
+
384
+ # If threads are still running, raise ForceShutdown and wait to finish.
385
+ @shutdown_mutex.synchronize do
386
+ @force_shutdown = true
387
+ threads.each do |t|
388
+ t.raise ForceShutdown if t[:with_force_shutdown]
389
+ end
390
+ end
391
+ join.call(@shutdown_grace_time)
392
+
393
+ # If threads are _still_ running, forcefully kill them and wait to finish.
394
+ threads.each(&:kill)
395
+ join.call(1)
396
+ end
397
+
398
+ @spawned = 0
399
+ @workers = []
400
+ end
401
+ end
402
+ end
data/lib/puma/util.rb ADDED
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri/common'
4
+
5
+ module Puma
6
+ module Util
7
+ module_function
8
+
9
+ def pipe
10
+ IO.pipe
11
+ end
12
+
13
+ # Escapes and unescapes a URI escaped string with
14
+ # +encoding+. +encoding+ will be the target encoding of the string
15
+ # returned, and it defaults to UTF-8
16
+ if defined?(::Encoding)
17
+ def escape(s, encoding = Encoding::UTF_8)
18
+ URI.encode_www_form_component(s, encoding)
19
+ end
20
+
21
+ def unescape(s, encoding = Encoding::UTF_8)
22
+ URI.decode_www_form_component(s, encoding)
23
+ end
24
+ else
25
+ def escape(s, encoding = nil)
26
+ URI.encode_www_form_component(s, encoding)
27
+ end
28
+
29
+ def unescape(s, encoding = nil)
30
+ URI.decode_www_form_component(s, encoding)
31
+ end
32
+ end
33
+ module_function :unescape, :escape
34
+
35
+ DEFAULT_SEP = /[&;] */n
36
+
37
+ # Stolen from Mongrel, with some small modifications:
38
+ # Parses a query string by breaking it up at the '&'
39
+ # and ';' characters. You can also use this to parse
40
+ # cookies by changing the characters used in the second
41
+ # parameter (which defaults to '&;').
42
+ def parse_query(qs, d = nil, &unescaper)
43
+ unescaper ||= method(:unescape)
44
+
45
+ params = {}
46
+
47
+ (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
48
+ next if p.empty?
49
+ k, v = p.split('=', 2).map(&unescaper)
50
+
51
+ if cur = params[k]
52
+ if cur.class == Array
53
+ params[k] << v
54
+ else
55
+ params[k] = [cur, v]
56
+ end
57
+ else
58
+ params[k] = v
59
+ end
60
+ end
61
+
62
+ params
63
+ end
64
+
65
+ # A case-insensitive Hash that preserves the original case of a
66
+ # header when set.
67
+ class HeaderHash < Hash
68
+ def self.new(hash={})
69
+ HeaderHash === hash ? hash : super(hash)
70
+ end
71
+
72
+ def initialize(hash={})
73
+ super()
74
+ @names = {}
75
+ hash.each { |k, v| self[k] = v }
76
+ end
77
+
78
+ def each
79
+ super do |k, v|
80
+ yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
81
+ end
82
+ end
83
+
84
+ # @!attribute [r] to_hash
85
+ def to_hash
86
+ hash = {}
87
+ each { |k,v| hash[k] = v }
88
+ hash
89
+ end
90
+
91
+ def [](k)
92
+ super(k) || super(@names[k.downcase])
93
+ end
94
+
95
+ def []=(k, v)
96
+ canonical = k.downcase
97
+ delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
98
+ @names[k] = @names[canonical] = k
99
+ super k, v
100
+ end
101
+
102
+ def delete(k)
103
+ canonical = k.downcase
104
+ result = super @names.delete(canonical)
105
+ @names.delete_if { |name,| name.downcase == canonical }
106
+ result
107
+ end
108
+
109
+ def include?(k)
110
+ @names.include?(k) || @names.include?(k.downcase)
111
+ end
112
+
113
+ alias_method :has_key?, :include?
114
+ alias_method :member?, :include?
115
+ alias_method :key?, :include?
116
+
117
+ def merge!(other)
118
+ other.each { |k, v| self[k] = v }
119
+ self
120
+ end
121
+
122
+ def merge(other)
123
+ hash = dup
124
+ hash.merge! other
125
+ end
126
+
127
+ def replace(other)
128
+ clear
129
+ other.each { |k, v| self[k] = v }
130
+ self
131
+ end
132
+ end
133
+ end
134
+ end