patient_http 1.6.0 → 1.7.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +8 -7
  3. data/CHANGELOG.md +39 -0
  4. data/README.md +540 -514
  5. data/VERSION +1 -1
  6. data/lib/patient_http/callback_args.rb +53 -48
  7. data/lib/patient_http/callback_validator.rb +11 -7
  8. data/lib/patient_http/class_helper.rb +6 -7
  9. data/lib/patient_http/client.rb +28 -22
  10. data/lib/patient_http/client_pool.rb +134 -39
  11. data/lib/patient_http/completion_executor.rb +20 -20
  12. data/lib/patient_http/configuration.rb +367 -119
  13. data/lib/patient_http/connection_endpoint.rb +150 -0
  14. data/lib/patient_http/encryptor.rb +28 -18
  15. data/lib/patient_http/error.rb +24 -18
  16. data/lib/patient_http/external_storage.rb +42 -38
  17. data/lib/patient_http/http_error.rb +30 -26
  18. data/lib/patient_http/http_headers.rb +57 -26
  19. data/lib/patient_http/immediate_retries.rb +98 -0
  20. data/lib/patient_http/inline_task_handler.rb +15 -10
  21. data/lib/patient_http/lifecycle_manager.rb +39 -40
  22. data/lib/patient_http/outgoing_request.rb +25 -23
  23. data/lib/patient_http/payload.rb +28 -26
  24. data/lib/patient_http/payload_store/active_record_store.rb +31 -34
  25. data/lib/patient_http/payload_store/base.rb +42 -46
  26. data/lib/patient_http/payload_store/file_store.rb +22 -26
  27. data/lib/patient_http/payload_store/redis_store.rb +28 -34
  28. data/lib/patient_http/payload_store/s3_store.rb +25 -28
  29. data/lib/patient_http/payload_store.rb +2 -0
  30. data/lib/patient_http/processor.rb +111 -79
  31. data/lib/patient_http/processor_observer.rb +65 -59
  32. data/lib/patient_http/rails/engine.rb +13 -8
  33. data/lib/patient_http/redirect_error.rb +50 -41
  34. data/lib/patient_http/redirect_helper.rb +38 -38
  35. data/lib/patient_http/request.rb +70 -46
  36. data/lib/patient_http/request_error.rb +47 -42
  37. data/lib/patient_http/request_helper.rb +142 -119
  38. data/lib/patient_http/request_preparer.rb +13 -10
  39. data/lib/patient_http/request_task.rb +113 -84
  40. data/lib/patient_http/request_template.rb +87 -64
  41. data/lib/patient_http/response.rb +58 -52
  42. data/lib/patient_http/response_reader.rb +66 -65
  43. data/lib/patient_http/secret_manager.rb +34 -30
  44. data/lib/patient_http/secret_reference.rb +33 -26
  45. data/lib/patient_http/synchronous_executor.rb +67 -95
  46. data/lib/patient_http/task_handler.rb +23 -19
  47. data/lib/patient_http/time_helper.rb +8 -8
  48. data/lib/patient_http.rb +311 -186
  49. data/patient_http.gemspec +3 -2
  50. metadata +20 -4
@@ -1,43 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PatientHttp
4
- # Represents an HTTP response from an async request.
4
+ # The HTTP response to an async request.
5
5
  #
6
- # This class encapsulates the response data including status, headers, body,
7
- # and metadata about the request that generated it.
6
+ # A response holds the status, headers, and body, and details about the
7
+ # request that returned it. Responses can be serialized to JSON, so they can
8
+ # go through a job queue.
8
9
  class Response
9
10
  UNDEFINED = Object.new.freeze
10
11
  private_constant :UNDEFINED
11
12
 
12
- # @return [Integer] HTTP status code
13
+ # @return [Integer] The HTTP status code.
13
14
  attr_reader :status
14
15
 
15
- # Response headers. Headers that appeared multiple times in the response
16
- # (such as set-cookie) are flattened into a single joined string value.
16
+ # The response headers. A header that occurs more than one time in the
17
+ # response, such as `set-cookie`, becomes one string with the values joined.
17
18
  #
18
- # @return [HttpHeaders] response headers
19
+ # @return [HttpHeaders] The response headers.
19
20
  attr_reader :headers
20
21
 
21
- # @return [Float] request duration in seconds
22
+ # @return [Float] The request duration in seconds.
22
23
  attr_reader :duration
23
24
 
24
- # @return [String] request ID
25
+ # @return [String] The request ID.
25
26
  attr_reader :request_id
26
27
 
27
- # @return [String] request URL
28
+ # @return [String] The request URL.
28
29
  attr_reader :url
29
30
 
30
- # @return [Symbol] HTTP method
31
+ # @return [Symbol] The HTTP method.
31
32
  attr_reader :http_method
32
33
 
33
- # @return [Array<String>] URLs visited during redirect chain (empty if no redirects)
34
+ # @return [Array<String>] The URLs of the redirects that were followed, in
35
+ # order. Empty if no redirects were followed.
34
36
  attr_reader :redirects
35
37
 
36
38
  class << self
37
- # Reconstruct a Response from a hash
39
+ # Creates a response from its serialized form.
38
40
  #
39
- # @param hash [Hash] hash representation
40
- # @return [Response] reconstructed response
41
+ # @param hash [Hash] The hash from {#as_json}.
42
+ # @return [Response] The response.
41
43
  def load(hash)
42
44
  new(
43
45
  status: hash["status"],
@@ -53,17 +55,18 @@ module PatientHttp
53
55
  end
54
56
  end
55
57
 
56
- # Initialize a Response from an Async::HTTP::Response
58
+ # Creates a response.
57
59
  #
58
- # @param status [Integer] HTTP status code
59
- # @param headers [Hash, HttpHeaders] response headers
60
- # @param body [String, nil] response body
61
- # @param duration [Float] request duration in seconds
62
- # @param request_id [String] the request ID
63
- # @param url [String] the request URL
64
- # @param http_method [Symbol] the HTTP method
65
- # @param callback_args [Hash, nil] callback arguments (string keys)
66
- # @param redirects [Array<String>, nil] URLs visited during redirect chain
60
+ # @param status [Integer] The HTTP status code.
61
+ # @param headers [Hash, HttpHeaders] The response headers.
62
+ # @param body [String, nil] The response body.
63
+ # @param duration [Float] The request duration in seconds.
64
+ # @param request_id [String] The request ID.
65
+ # @param url [String] The request URL.
66
+ # @param http_method [Symbol] The HTTP method.
67
+ # @param callback_args [Hash, nil] The callback arguments, with string keys.
68
+ # @param redirects [Array<String>, nil] The URLs of the redirects that were
69
+ # followed.
67
70
  def initialize(status:, headers:, body:, duration:, request_id:, url:, http_method:, callback_args: nil, redirects: nil)
68
71
  @status = status
69
72
  @headers = HttpHeaders.new(headers)
@@ -80,76 +83,78 @@ module PatientHttp
80
83
  @redirects = redirects || []
81
84
  end
82
85
 
83
- # Returns the callback arguments as a CallbackArgs object.
86
+ # Returns the callback arguments that were passed with the request.
84
87
  #
85
- # @return [CallbackArgs] the callback arguments
88
+ # @return [CallbackArgs] The callback arguments.
86
89
  def callback_args
87
90
  @callback_args ||= CallbackArgs.load(@callback_args_data)
88
91
  end
89
92
 
90
- # Returns the response body, decoding it from the payload if necessary.
93
+ # Returns the response body. The body is decoded on first access.
91
94
  #
92
- # @return [String, nil] The decoded response body or nil if there was no body.
95
+ # @return [String, nil] The response body, or `nil` if the response has no
96
+ # body.
93
97
  def body
94
98
  @body = @payload&.value if @body.equal?(UNDEFINED)
95
99
  @body
96
100
  end
97
101
 
98
- # Check if response is successful (2xx status)
102
+ # Returns whether the status is 2xx.
99
103
  #
100
- # @return [Boolean]
104
+ # @return [Boolean] `true` if the request succeeded.
101
105
  def success?
102
106
  status >= 200 && status < 300
103
107
  end
104
108
 
105
- # Check if response is a redirect (3xx status)
109
+ # Returns whether the status is 3xx.
106
110
  #
107
- # @return [Boolean]
111
+ # @return [Boolean] `true` if the response is a redirect.
108
112
  def redirect?
109
113
  status >= 300 && status < 400
110
114
  end
111
115
 
112
- # Check if response is a client error (4xx status)
116
+ # Returns whether the status is 4xx.
113
117
  #
114
- # @return [Boolean]
118
+ # @return [Boolean] `true` if the response is a client error.
115
119
  def client_error?
116
120
  status >= 400 && status < 500
117
121
  end
118
122
 
119
- # Check if response is a server error (5xx status)
123
+ # Returns whether the status is 5xx.
120
124
  #
121
- # @return [Boolean]
125
+ # @return [Boolean] `true` if the response is a server error.
122
126
  def server_error?
123
127
  status >= 500 && status < 600
124
128
  end
125
129
 
126
- # Check if response is any error (4xx or 5xx status)
130
+ # Returns whether the status is 4xx or 5xx.
127
131
  #
128
- # @return [Boolean]
132
+ # @return [Boolean] `true` if the response is an error.
129
133
  def error?
130
134
  status >= 400 && status < 600
131
135
  end
132
136
 
133
- # Get the Content-Type header
137
+ # Returns the value of the `content-type` header.
134
138
  #
135
- # @return [String, nil]
139
+ # @return [String, nil] The content type, or `nil` if the header isn't set.
136
140
  def content_type
137
141
  headers["content-type"]
138
142
  end
139
143
 
140
- # Return true if Content-Type indicates JSON.
144
+ # Returns whether the `content-type` header identifies a JSON body.
141
145
  #
142
- # @return [Boolean]
146
+ # @return [Boolean] `true` if the body is JSON.
143
147
  def json?
144
148
  type = content_type.to_s.downcase
145
149
  type.match?(%r{\Aapplication/[^ ]*json\b}) || type == "text/json"
146
150
  end
147
151
 
148
- # Parse response body as JSON
152
+ # Parses the response body as JSON.
149
153
  #
150
- # @return [Hash, Array] parsed JSON
151
- # @raise [RuntimeError] if Content-Type is not application/json
152
- # @raise [JSON::ParserError] if body is not valid JSON
154
+ # @return [Hash, Array] The parsed body.
155
+ # @raise [RuntimeError] If the `content-type` header doesn't identify a JSON
156
+ # body.
157
+ # @raise [JSON::ParserError] If the body isn't valid JSON.
153
158
  def json
154
159
  unless json?
155
160
  raise "Response Content-Type is not application/json (got: #{content_type.inspect})"
@@ -158,9 +163,9 @@ module PatientHttp
158
163
  JSON.parse(body)
159
164
  end
160
165
 
161
- # Serialize to JSON hash.
166
+ # Returns the response as a JSON-compatible hash.
162
167
  #
163
- # @return [Hash]
168
+ # @return [Hash] The serialized response.
164
169
  def as_json
165
170
  {
166
171
  "status" => status,
@@ -175,10 +180,11 @@ module PatientHttp
175
180
  }
176
181
  end
177
182
 
178
- # Serialize to JSON string.
183
+ # Returns the response as a JSON string.
179
184
  #
180
- # @param options [Hash] options to pass to JSON.generate (for ActiveSupport compatibility)
181
- # @return [String] JSON representation
185
+ # @param options [Hash, nil] The options for `JSON.generate`. This parameter
186
+ # makes the method compatible with Active Support.
187
+ # @return [String] The JSON string.
182
188
  def to_json(options = nil)
183
189
  JSON.generate(as_json, options)
184
190
  end
@@ -3,11 +3,12 @@
3
3
  module PatientHttp
4
4
  # Reads and decodes HTTP response bodies.
5
5
  #
6
- # Reading happens on the reactor thread and collects the raw (possibly
7
- # compressed) body chunks with size validation. Decoding — joining the
8
- # chunks, inflating compressed content, and applying the charset — is a
9
- # separate step so it can run on a completion worker thread instead of
10
- # blocking the event loop.
6
+ # The reactor thread reads the raw body chunks, which can be compressed, and
7
+ # checks their size. Decoding is a separate step that runs on a completion
8
+ # worker thread, so it doesn't block the reactor. Decoding joins the chunks,
9
+ # decompresses them, and applies the charset.
10
+ #
11
+ # @api private
11
12
  class ResponseReader
12
13
  # Raised when a body read is aborted because the processor was stopped
13
14
  # past its shutdown deadline. The shutdown sequence re-enqueues the task,
@@ -34,7 +35,7 @@ module PatientHttp
34
35
  IDENTITY_ENCODING = "identity"
35
36
 
36
37
  class << self
37
- # Split the encodings named in the content-encoding header into the ones
38
+ # Splits the encodings named in the content-encoding header into the ones
38
39
  # that stay applied to the body and the ones that can be decoded.
39
40
  #
40
41
  # A body can carry more than one encoding. They are listed in the order
@@ -42,9 +43,9 @@ module PatientHttp
42
43
  # stops at the first name it does not recognize. Everything before that
43
44
  # point stays applied to the body.
44
45
  #
45
- # @param headers_hash [Hash] the response headers
46
- # @return [Array(Array<String>, Array<String>)] the encodings that remain
47
- # applied and the encodings that can be decoded, both in applied order
46
+ # @param headers_hash [Hash] The response headers.
47
+ # @return [Array(Array<String>, Array<String>)] The encodings that remain
48
+ # applied and the encodings that can be decoded, both in applied order.
48
49
  def split_encodings(headers_hash)
49
50
  encodings = content_encodings(headers_hash)
50
51
  boundary = encodings.rindex { |name| !decodable?(name) }
@@ -53,10 +54,10 @@ module PatientHttp
53
54
  [encodings[0..boundary], encodings[(boundary + 1)..]]
54
55
  end
55
56
 
56
- # Parse the content-encoding header into encoding names.
57
+ # Parses the content-encoding header into encoding names.
57
58
  #
58
- # @param headers_hash [Hash] the response headers
59
- # @return [Array<String>] the lowercased encoding names in applied order
59
+ # @param headers_hash [Hash] The response headers.
60
+ # @return [Array<String>] The lowercased encoding names in applied order.
60
61
  def content_encodings(headers_hash)
61
62
  headers_hash["content-encoding"].to_s.split(",").filter_map do |name|
62
63
  name = name.strip.downcase
@@ -64,19 +65,19 @@ module PatientHttp
64
65
  end
65
66
  end
66
67
 
67
- # @param name [String] a lowercased content encoding name
68
- # @return [Boolean] true if the reader can remove this encoding
68
+ # @param name [String] A lowercased content encoding name.
69
+ # @return [Boolean] `true` if the reader can remove this encoding.
69
70
  def decodable?(name)
70
71
  name == IDENTITY_ENCODING || INFLATE_WINDOW_BITS.key?(name)
71
72
  end
72
73
 
73
- # Restate the content-encoding header for a decoded body. The header is
74
+ # Restates the content-encoding header for a decoded body. The header is
74
75
  # removed when nothing is left applied, and narrowed to the encodings the
75
76
  # reader could not remove otherwise, so the header always describes the
76
77
  # body delivered with it.
77
78
  #
78
- # @param headers_hash [Hash] the response headers
79
- # @return [Hash] the headers with content-encoding updated or removed
79
+ # @param headers_hash [Hash] The response headers.
80
+ # @return [Hash] The headers with content-encoding updated or removed.
80
81
  def rewrite_content_encoding(headers_hash)
81
82
  return headers_hash unless headers_hash.key?("content-encoding")
82
83
 
@@ -90,21 +91,21 @@ module PatientHttp
90
91
  end
91
92
  end
92
93
 
93
- # Initialize the reader.
94
+ # Creates the reader.
94
95
  #
95
96
  # Reading needs a processor so it can abort once the processor is past its
96
97
  # shutdown deadline. Decoding needs only the configuration, so a caller
97
98
  # that does its own reading can supply the configuration on its own.
98
99
  #
99
- # @param processor [Processor, nil] the processor object
100
- # @param config [Configuration, nil] the configuration; defaults to the
101
- # processor's configuration
100
+ # @param processor [Processor, nil] The processor object.
101
+ # @param config [Configuration, nil] The configuration; defaults to the
102
+ # processor's configuration.
102
103
  def initialize(processor, config: nil)
103
104
  @processor = processor
104
105
  @config = config || processor.config
105
106
  end
106
107
 
107
- # Read the raw response body chunks with size validation.
108
+ # Reads the raw response body chunks with size validation.
108
109
  #
109
110
  # Reads the async HTTP response body asynchronously to completion, which allows
110
111
  # the connection to be reused. The async-http client handles connection pooling
@@ -118,11 +119,11 @@ module PatientHttp
118
119
  # the Content-Length of the resource, so the header check is skipped when
119
120
  # the body reports itself as empty.
120
121
  #
121
- # @param async_response [Async::HTTP::Protocol::Response] the async HTTP response
122
- # @param headers_hash [Hash] the response headers
123
- # @return [Array<String>, nil] the raw body chunks or nil if no body present
124
- # @raise [ResponseTooLargeError] if the body exceeds max_response_size
125
- # @raise [ReadAbortedError] if the processor stopped past its shutdown deadline mid-read
122
+ # @param async_response [Async::HTTP::Protocol::Response] The async HTTP response.
123
+ # @param headers_hash [Hash] The response headers.
124
+ # @return [Array<String>, nil] The raw body chunks or nil if no body present.
125
+ # @raise [ResponseTooLargeError] If the body exceeds max_response_size.
126
+ # @raise [ReadAbortedError] If the processor stopped past its shutdown deadline mid-read.
126
127
  def read_raw_body(async_response, headers_hash)
127
128
  body = async_response.body
128
129
  return nil unless body
@@ -131,7 +132,7 @@ module PatientHttp
131
132
  read_body_chunks(async_response)
132
133
  end
133
134
 
134
- # Decode raw body chunks into the final body string.
135
+ # Decodes raw body chunks into the final body string.
135
136
  #
136
137
  # Joins the chunks, inflates gzip/deflate content (enforcing
137
138
  # max_response_size on the inflated bytes), and applies the charset from
@@ -144,10 +145,10 @@ module PatientHttp
144
145
  # so the content-encoding header delivered with the response describes the
145
146
  # body it carries.
146
147
  #
147
- # @param chunks [Array<String>, nil] the raw body chunks
148
- # @param headers_hash [Hash] the response headers
149
- # @return [String, nil] the decoded body or nil if there was no body
150
- # @raise [ResponseTooLargeError] if the inflated body exceeds max_response_size
148
+ # @param chunks [Array<String>, nil] The raw body chunks.
149
+ # @param headers_hash [Hash] The response headers.
150
+ # @return [String, nil] The decoded body or nil if there was no body.
151
+ # @raise [ResponseTooLargeError] If the inflated body exceeds max_response_size.
151
152
  def decode_body(chunks, headers_hash)
152
153
  return nil if chunks.nil?
153
154
 
@@ -165,13 +166,13 @@ module PatientHttp
165
166
 
166
167
  private
167
168
 
168
- # Remove the given encodings from the body, starting with the one applied
169
+ # Removes the given encodings from the body, starting with the one applied
169
170
  # last. Identity needs no work; every other name here inflates.
170
171
  #
171
- # @param chunks [Array<String>] the encoded body chunks
172
- # @param encodings [Array<String>] decodable encoding names in applied order
173
- # @return [Array<String>] the decoded chunks
174
- # @raise [ResponseTooLargeError] if the inflated body exceeds max_response_size
172
+ # @param chunks [Array<String>] The encoded body chunks.
173
+ # @param encodings [Array<String>] Decodable encoding names in applied order.
174
+ # @return [Array<String>] The decoded chunks.
175
+ # @raise [ResponseTooLargeError] If the inflated body exceeds max_response_size.
175
176
  def inflate_encodings(chunks, encodings)
176
177
  encodings.reverse_each do |name|
177
178
  chunks = [inflate_encoding(chunks, name)] if INFLATE_WINDOW_BITS.key?(name)
@@ -180,15 +181,15 @@ module PatientHttp
180
181
  chunks
181
182
  end
182
183
 
183
- # Inflate one encoding, trying each wire format the encoding can use. The
184
+ # Inflates one encoding, trying each wire format the encoding can use. The
184
185
  # chunks are all in memory, so a format that turns out to be wrong can be
185
186
  # abandoned and the next one started from the beginning of the body.
186
187
  #
187
- # @param chunks [Array<String>] the encoded body chunks
188
- # @param name [String] a lowercased content encoding name
189
- # @return [String] the inflated body
190
- # @raise [Zlib::Error] if no format could inflate the body
191
- # @raise [ResponseTooLargeError] if the inflated body exceeds max_response_size
188
+ # @param chunks [Array<String>] The encoded body chunks.
189
+ # @param name [String] A lowercased content encoding name.
190
+ # @return [String] The inflated body.
191
+ # @raise [Zlib::Error] If no format could inflate the body.
192
+ # @raise [ResponseTooLargeError] If the inflated body exceeds max_response_size.
192
193
  def inflate_encoding(chunks, name)
193
194
  formats = INFLATE_WINDOW_BITS.fetch(name)
194
195
  last_index = formats.size - 1
@@ -200,11 +201,11 @@ module PatientHttp
200
201
  end
201
202
  end
202
203
 
203
- # Report an encoding that could not be removed. The body is still delivered
204
+ # Reports an encoding that could not be removed. The body is still delivered
204
205
  # with its content-encoding header, so the caller can decode it, but the
205
206
  # server ignored the accept-encoding header and that is worth recording.
206
207
  #
207
- # @param remaining [Array<String>] the encodings left on the body
208
+ # @param remaining [Array<String>] The encodings left on the body.
208
209
  # @return [void]
209
210
  def warn_undecodable(remaining)
210
211
  logger&.warn(
@@ -222,10 +223,10 @@ module PatientHttp
222
223
  @config.logger
223
224
  end
224
225
 
225
- # Validate content-length header doesn't exceed max size.
226
+ # Validates content-length header doesn't exceed max size.
226
227
  #
227
- # @param headers_hash [Hash] the response headers
228
- # @raise [ResponseTooLargeError] if content-length exceeds max_response_size
228
+ # @param headers_hash [Hash] The response headers.
229
+ # @raise [ResponseTooLargeError] If content-length exceeds max_response_size.
229
230
  def validate_content_length(headers_hash)
230
231
  content_length = headers_hash["content-length"]&.to_i
231
232
  if content_length && content_length > max_response_size
@@ -235,12 +236,12 @@ module PatientHttp
235
236
  end
236
237
  end
237
238
 
238
- # Read body chunks while checking size.
239
+ # Reads body chunks while checking size.
239
240
  #
240
- # @param async_response [Async::HTTP::Protocol::Response] the async HTTP response
241
- # @return [Array<String>] the raw body chunks
242
- # @raise [ResponseTooLargeError] if body size exceeds max_response_size during read
243
- # @raise [ReadAbortedError] if the processor stopped past its shutdown deadline mid-read
241
+ # @param async_response [Async::HTTP::Protocol::Response] The async HTTP response.
242
+ # @return [Array<String>] The raw body chunks.
243
+ # @raise [ResponseTooLargeError] If body size exceeds max_response_size during read.
244
+ # @raise [ReadAbortedError] If the processor stopped past its shutdown deadline mid-read.
244
245
  def read_body_chunks(async_response)
245
246
  chunks = []
246
247
  total_size = 0
@@ -277,13 +278,13 @@ module PatientHttp
277
278
  end
278
279
  end
279
280
 
280
- # Inflate compressed body chunks with streaming size enforcement, so a
281
+ # Inflates compressed body chunks with streaming size enforcement, so a
281
282
  # small compressed body cannot expand past max_response_size.
282
283
  #
283
- # @param chunks [Array<String>] the raw compressed chunks
284
- # @param window_bits [Integer] Zlib window bits for the content encoding
285
- # @return [String] the inflated body
286
- # @raise [ResponseTooLargeError] if the inflated size exceeds max_response_size
284
+ # @param chunks [Array<String>] The raw compressed chunks.
285
+ # @param window_bits [Integer] Zlib window bits for the content encoding.
286
+ # @return [String] The inflated body.
287
+ # @raise [ResponseTooLargeError] If the inflated size exceeds max_response_size.
287
288
  def inflate_chunks(chunks, window_bits)
288
289
  # A response can declare a content encoding and still carry no body.
289
290
  # There is nothing to inflate, and finishing an empty stream would
@@ -320,10 +321,10 @@ module PatientHttp
320
321
  end
321
322
  end
322
323
 
323
- # Extract charset from Content-Type header.
324
+ # Extracts charset from Content-Type header.
324
325
  #
325
- # @param headers_hash [Hash] the response headers
326
- # @return [String, nil] the charset name or nil if not specified
326
+ # @param headers_hash [Hash] The response headers.
327
+ # @return [String, nil] The charset name or nil if not specified.
327
328
  def extract_charset(headers_hash)
328
329
  content_type = headers_hash["content-type"]
329
330
  return nil unless content_type
@@ -335,14 +336,14 @@ module PatientHttp
335
336
  charset.gsub(/\A["']|["']\z/, "")
336
337
  end
337
338
 
338
- # Apply charset encoding to response body.
339
+ # Applies charset encoding to response body.
339
340
  #
340
341
  # Sets the string encoding based on the charset specified in the Content-Type header.
341
342
  # Falls back to ASCII-8BIT if charset is invalid or not recognized.
342
343
  #
343
- # @param body [String] the response body
344
- # @param headers_hash [Hash] the response headers
345
- # @return [String] the body with proper encoding set
344
+ # @param body [String] The response body.
345
+ # @param headers_hash [Hash] The response headers.
346
+ # @return [String] The body with proper encoding set.
346
347
  def apply_charset_encoding(body, headers_hash)
347
348
  return body unless body
348
349
 
@@ -1,41 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PatientHttp
4
- # Resolves {SecretReference} values into their actual secret values when a request
5
- # is sent by the processor.
4
+ # Replaces {SecretReference} values with the secret values when the processor
5
+ # sends a request.
6
6
  #
7
- # A SecretManager is built from the secrets registered on the {Configuration}.
7
+ # The manager holds the secrets registered in the {Configuration}.
8
8
  #
9
9
  # @see Configuration#secret_manager
10
10
  class SecretManager
11
- # Raised when a referenced secret cannot be resolved.
11
+ # Raised when a referenced secret isn't registered.
12
12
  class SecretNotFoundError < StandardError; end
13
13
 
14
- # Initialize a new SecretManager.
14
+ # Creates a secret manager.
15
15
  #
16
- # @param secrets [Hash{String => Object}] static registry mapping names to values
17
- # (a value may be a callable, which is invoked with the name to produce the value)
18
- # secret not found in the static registry
16
+ # @param secrets [Hash{String => Object}] The secret values, keyed by name. A
17
+ # value can be a callable, which is called with the name to get the value.
19
18
  def initialize(secrets: {})
20
19
  @secrets = secrets || {}
21
20
  end
22
21
 
23
- # Check if a secret name is registered in the static registry.
22
+ # Returns whether a secret is registered.
24
23
  #
25
- # @param name [String, Symbol] the secret name
26
- # @return [Boolean] true if the name is registered, false otherwise
24
+ # @param name [String, Symbol] The secret name.
25
+ # @return [Boolean] `true` if the secret is registered.
27
26
  def include?(name)
28
27
  @secrets.include?(name.to_s)
29
28
  end
30
29
 
31
- # Resolve a secret by name.
30
+ # Returns the value of a secret. If the registered value responds to `call`,
31
+ # it's called with the name.
32
32
  #
33
- # The static registry is checked first; if the registered value responds to #call
34
- # it is invoked with the name. If the name is not in the registry, an error is raised.
35
- #
36
- # @param name [String, Symbol] the secret name
37
- # @return [String] the resolved secret value
38
- # @raise [SecretNotFoundError] if the secret cannot be resolved
33
+ # @param name [String, Symbol] The secret name.
34
+ # @return [String, nil] The secret value.
35
+ # @raise [SecretNotFoundError] If the secret isn't registered.
39
36
  def resolve(name)
40
37
  name = name.to_s
41
38
 
@@ -48,27 +45,34 @@ module PatientHttp
48
45
  value&.to_s
49
46
  end
50
47
 
51
- # Resolve any secret references in a headers hash, returning a new hash.
48
+ # Returns a copy of the headers with the secret references replaced by the
49
+ # secret values.
52
50
  #
53
- # @param headers [Hash, nil] header name/value pairs
54
- # @return [Hash, nil] a new hash with secret references replaced by resolved values
51
+ # @param headers [Hash, nil] The headers.
52
+ # @return [Hash, nil] The resolved headers.
53
+ # @raise [SecretNotFoundError] If a referenced secret isn't registered.
55
54
  def resolve_headers(headers)
56
55
  resolve_values(headers)
57
56
  end
58
57
 
59
- # Resolve any secret references in a params hash, returning a new hash.
58
+ # Returns a copy of the query parameters with the secret references
59
+ # replaced by the secret values.
60
60
  #
61
- # @param params [Hash, nil] param name/value pairs
62
- # @return [Hash, nil] a new hash with secret references replaced by resolved values
61
+ # @param params [Hash, nil] The query parameters.
62
+ # @return [Hash, nil] The resolved query parameters.
63
+ # @raise [SecretNotFoundError] If a referenced secret isn't registered.
63
64
  def resolve_params(params)
64
65
  resolve_values(params)
65
66
  end
66
67
 
67
- # Append resolved secret params to a URL's query string.
68
+ # Adds the resolved secret query parameters to a URL.
68
69
  #
69
- # @param url [String] the request URL
70
- # @param secret_params [Hash, nil] secret param name/value (SecretReference) pairs
71
- # @return [String] the URL with resolved secret params appended (unchanged if none)
70
+ # @param url [String] The request URL.
71
+ # @param secret_params [Hash, nil] The query parameters whose values are
72
+ # secret references.
73
+ # @return [String] The URL with the parameters added. If there are no
74
+ # parameters, the URL is unchanged.
75
+ # @raise [SecretNotFoundError] If a referenced secret isn't registered.
72
76
  def resolve_url(url, secret_params)
73
77
  return url if secret_params.nil? || secret_params.empty?
74
78
 
@@ -80,8 +84,8 @@ module PatientHttp
80
84
 
81
85
  private
82
86
 
83
- # Return a new hash with any secret-reference values replaced by their resolved
84
- # values. Non-secret values are passed through unchanged.
87
+ # Returns a copy of a hash with the secret references replaced by the
88
+ # secret values. Other values are unchanged.
85
89
  def resolve_values(hash)
86
90
  return hash if hash.nil?
87
91