api_adaptor 0.1.0 → 1.0.1
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -3
- data/.yardopts +10 -0
- data/CHANGELOG.md +36 -1
- data/CLAUDE.md +423 -0
- data/Gemfile.lock +23 -14
- data/README.md +85 -0
- data/Rakefile +7 -1
- data/lib/api_adaptor/base.rb +137 -0
- data/lib/api_adaptor/exceptions.rb +67 -4
- data/lib/api_adaptor/headers.rb +32 -0
- data/lib/api_adaptor/json_client.rb +172 -3
- data/lib/api_adaptor/list_response.rb +63 -21
- data/lib/api_adaptor/null_logger.rb +53 -39
- data/lib/api_adaptor/response.rb +107 -10
- data/lib/api_adaptor/variables.rb +37 -0
- data/lib/api_adaptor/version.rb +1 -1
- data/lib/api_adaptor.rb +31 -1
- metadata +46 -2
data/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# ApiAdaptor
|
|
2
2
|
|
|
3
|
+
[](https://github.com/huwd/api_adaptor/actions/workflows/quality-checks.yml)
|
|
4
|
+
[](https://badge.fury.io/rb/api_adaptor)
|
|
5
|
+
[](https://huwd.github.io/api_adaptor/)
|
|
6
|
+
|
|
3
7
|
A basic adaptor to send HTTP requests and parse the responses.
|
|
4
8
|
Intended to bootstrap the quick writing of Adaptors for specific APIs, without having to write the same old JSON request and processing time and time again.
|
|
5
9
|
|
|
@@ -17,6 +21,17 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
|
17
21
|
gem install api_adaptor
|
|
18
22
|
```
|
|
19
23
|
|
|
24
|
+
## API Documentation
|
|
25
|
+
|
|
26
|
+
Full API documentation is available at [https://huwd.github.io/api_adaptor/](https://huwd.github.io/api_adaptor/)
|
|
27
|
+
|
|
28
|
+
Generate documentation locally:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bundle exec yard # Generate docs to doc/
|
|
32
|
+
bundle exec yard server # Preview at http://localhost:8808
|
|
33
|
+
```
|
|
34
|
+
|
|
20
35
|
## Releasing
|
|
21
36
|
|
|
22
37
|
Publishing is handled by GitHub Actions when you push a version tag.
|
|
@@ -196,6 +211,76 @@ User agent would read
|
|
|
196
211
|
test_app/1.0.0 (contact@example.com)
|
|
197
212
|
```
|
|
198
213
|
|
|
214
|
+
## Development
|
|
215
|
+
|
|
216
|
+
After checking out the repo, run `bundle install` to install dependencies.
|
|
217
|
+
|
|
218
|
+
### Running Tests
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
bundle exec rspec # Run tests only
|
|
222
|
+
bundle exec rubocop # Run linter only
|
|
223
|
+
bundle exec rake # Run tests, linter, and build docs
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Code Coverage
|
|
227
|
+
|
|
228
|
+
SimpleCov tracks test coverage. View the report at `coverage/index.html` after running tests.
|
|
229
|
+
|
|
230
|
+
Current coverage: 92.3% line, 81.65% branch
|
|
231
|
+
|
|
232
|
+
### Documentation
|
|
233
|
+
|
|
234
|
+
Generate YARD documentation:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
bundle exec yard # Generate docs to doc/
|
|
238
|
+
bundle exec yard server # Preview at http://localhost:8808
|
|
239
|
+
bundle exec yard stats # View documentation coverage
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Quality Standards
|
|
243
|
+
|
|
244
|
+
- **Tests:** RSpec with WebMock for HTTP mocking
|
|
245
|
+
- **Linting:** RuboCop with rubocop-yard for documentation
|
|
246
|
+
- **Coverage:** SimpleCov (minimum 80% line, 75% branch)
|
|
247
|
+
- **Documentation:** YARD for all public APIs
|
|
248
|
+
|
|
249
|
+
## Troubleshooting
|
|
250
|
+
|
|
251
|
+
### Redirects Not Following
|
|
252
|
+
|
|
253
|
+
By default, only GET/HEAD requests follow redirects. For POST/PUT/PATCH/DELETE:
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
client = MyApi.new("https://example.com", follow_non_get_redirects: true)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Timeout Errors
|
|
260
|
+
|
|
261
|
+
Default timeout is 4 seconds. Configure longer timeouts:
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
client = MyApi.new("https://example.com", timeout: 10)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Cross-Origin Redirect Security
|
|
268
|
+
|
|
269
|
+
By default, auth headers are stripped on cross-origin redirects. To allow (use with caution):
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
client = MyApi.new(
|
|
273
|
+
"https://example.com",
|
|
274
|
+
forward_auth_on_cross_origin_redirects: true # Security risk!
|
|
275
|
+
)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Or disable cross-origin redirects entirely:
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
client = MyApi.new("https://example.com", allow_cross_origin_redirects: false)
|
|
282
|
+
```
|
|
283
|
+
|
|
199
284
|
## Contributing
|
|
200
285
|
|
|
201
286
|
Bug reports and pull requests are welcome on GitHub at <https://github.com/huwd/api_adaptor>. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/huwd/api_adaptor/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
CHANGED
data/lib/api_adaptor/base.rb
CHANGED
|
@@ -6,20 +6,113 @@ require_relative "null_logger"
|
|
|
6
6
|
require_relative "list_response"
|
|
7
7
|
|
|
8
8
|
module ApiAdaptor
|
|
9
|
+
# Base class for building API-specific clients.
|
|
10
|
+
#
|
|
11
|
+
# Provides common functionality for JSON API clients including HTTP method delegation,
|
|
12
|
+
# URL construction, and pagination support. Subclass this to create clients for specific APIs.
|
|
13
|
+
#
|
|
14
|
+
# @example Creating a custom API client
|
|
15
|
+
# class MyApiClient < ApiAdaptor::Base
|
|
16
|
+
# def initialize
|
|
17
|
+
# super("https://api.example.com", bearer_token: "abc123")
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# def get_user(id)
|
|
21
|
+
# get_json("/users/#{id}")
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# def list_posts(page: 1)
|
|
25
|
+
# get_list("/posts?page=#{page}")
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# @example Using default options
|
|
30
|
+
# ApiAdaptor::Base.default_options = { timeout: 10 }
|
|
31
|
+
# client = MyApiClient.new # Inherits 10-second timeout
|
|
32
|
+
#
|
|
33
|
+
# @see JSONClient for underlying HTTP client options
|
|
9
34
|
class Base
|
|
35
|
+
# Raised when an invalid API URL is provided
|
|
10
36
|
class InvalidAPIURL < StandardError
|
|
11
37
|
end
|
|
12
38
|
|
|
13
39
|
extend Forwardable
|
|
14
40
|
|
|
41
|
+
# Returns the underlying JSONClient instance, creating it if necessary
|
|
42
|
+
#
|
|
43
|
+
# @return [JSONClient] The HTTP client instance
|
|
15
44
|
def client
|
|
16
45
|
@client ||= create_client
|
|
17
46
|
end
|
|
18
47
|
|
|
48
|
+
# Creates a new JSONClient with the configured options
|
|
49
|
+
#
|
|
50
|
+
# @return [JSONClient] A new HTTP client instance
|
|
19
51
|
def create_client
|
|
20
52
|
ApiAdaptor::JsonClient.new(options)
|
|
21
53
|
end
|
|
22
54
|
|
|
55
|
+
# @!method get_json(url, &block)
|
|
56
|
+
# Performs a GET request and parses JSON response
|
|
57
|
+
# @param url [String] The URL to request
|
|
58
|
+
# @yield [Hash] The parsed JSON response
|
|
59
|
+
# @return [Response, Object] Response object or yielded value
|
|
60
|
+
# @see JSONClient#get_json
|
|
61
|
+
#
|
|
62
|
+
# @!method post_json(url, params = {})
|
|
63
|
+
# Performs a POST request with JSON body
|
|
64
|
+
# @param url [String] The URL to request
|
|
65
|
+
# @param params [Hash] Data to send as JSON
|
|
66
|
+
# @return [Response] Response object
|
|
67
|
+
# @see JSONClient#post_json
|
|
68
|
+
#
|
|
69
|
+
# @!method put_json(url, params = {})
|
|
70
|
+
# Performs a PUT request with JSON body
|
|
71
|
+
# @param url [String] The URL to request
|
|
72
|
+
# @param params [Hash] Data to send as JSON
|
|
73
|
+
# @return [Response] Response object
|
|
74
|
+
# @see JSONClient#put_json
|
|
75
|
+
#
|
|
76
|
+
# @!method patch_json(url, params = {})
|
|
77
|
+
# Performs a PATCH request with JSON body
|
|
78
|
+
# @param url [String] The URL to request
|
|
79
|
+
# @param params [Hash] Data to send as JSON
|
|
80
|
+
# @return [Response] Response object
|
|
81
|
+
# @see JSONClient#patch_json
|
|
82
|
+
#
|
|
83
|
+
# @!method delete_json(url, params = {})
|
|
84
|
+
# Performs a DELETE request
|
|
85
|
+
# @param url [String] The URL to request
|
|
86
|
+
# @param params [Hash] Optional data to send as JSON
|
|
87
|
+
# @return [Response] Response object
|
|
88
|
+
# @see JSONClient#delete_json
|
|
89
|
+
#
|
|
90
|
+
# @!method get_raw(url)
|
|
91
|
+
# Performs a GET request and returns raw response
|
|
92
|
+
# @param url [String] The URL to request
|
|
93
|
+
# @return [RestClient::Response] Raw response object
|
|
94
|
+
# @see JSONClient#get_raw
|
|
95
|
+
#
|
|
96
|
+
# @!method get_raw!(url)
|
|
97
|
+
# Performs a GET request and returns raw response, raising on errors
|
|
98
|
+
# @param url [String] The URL to request
|
|
99
|
+
# @return [RestClient::Response] Raw response object
|
|
100
|
+
# @raise [HTTPClientError, HTTPServerError] On HTTP errors
|
|
101
|
+
# @see JSONClient#get_raw!
|
|
102
|
+
#
|
|
103
|
+
# @!method put_multipart(url, params = {})
|
|
104
|
+
# Performs a PUT request with multipart/form-data
|
|
105
|
+
# @param url [String] The URL to request
|
|
106
|
+
# @param params [Hash] Multipart form data
|
|
107
|
+
# @return [Response] Response object
|
|
108
|
+
# @see JSONClient#put_multipart
|
|
109
|
+
#
|
|
110
|
+
# @!method post_multipart(url, params = {})
|
|
111
|
+
# Performs a POST request with multipart/form-data
|
|
112
|
+
# @param url [String] The URL to request
|
|
113
|
+
# @param params [Hash] Multipart form data
|
|
114
|
+
# @return [Response] Response object
|
|
115
|
+
# @see JSONClient#post_multipart
|
|
23
116
|
def_delegators :client,
|
|
24
117
|
:get_json,
|
|
25
118
|
:post_json,
|
|
@@ -31,17 +124,40 @@ module ApiAdaptor
|
|
|
31
124
|
:put_multipart,
|
|
32
125
|
:post_multipart
|
|
33
126
|
|
|
127
|
+
# @return [Hash] The client configuration options
|
|
34
128
|
attr_reader :options
|
|
35
129
|
|
|
36
130
|
class << self
|
|
131
|
+
# @!attribute [w] logger
|
|
132
|
+
# Sets the default logger for all Base instances
|
|
133
|
+
# @param value [Logger] Logger instance
|
|
37
134
|
attr_writer :logger
|
|
135
|
+
|
|
136
|
+
# @!attribute [rw] default_options
|
|
137
|
+
# Default options merged into all Base instances
|
|
138
|
+
# @return [Hash, nil] Default options hash
|
|
38
139
|
attr_accessor :default_options
|
|
39
140
|
end
|
|
40
141
|
|
|
142
|
+
# Returns the default logger for Base instances
|
|
143
|
+
#
|
|
144
|
+
# @return [Logger] Logger instance (defaults to NullLogger)
|
|
41
145
|
def self.logger
|
|
42
146
|
@logger ||= ApiAdaptor::NullLogger.new
|
|
43
147
|
end
|
|
44
148
|
|
|
149
|
+
# Initializes a new API client
|
|
150
|
+
#
|
|
151
|
+
# @param endpoint_url [String, nil] Base URL for the API
|
|
152
|
+
# @param options [Hash] Configuration options (see JSONClient#initialize for details)
|
|
153
|
+
#
|
|
154
|
+
# @raise [InvalidAPIURL] If endpoint_url is invalid
|
|
155
|
+
#
|
|
156
|
+
# @example Basic initialization
|
|
157
|
+
# client = Base.new("https://api.example.com")
|
|
158
|
+
#
|
|
159
|
+
# @example With authentication
|
|
160
|
+
# client = Base.new("https://api.example.com", bearer_token: "abc123")
|
|
45
161
|
def initialize(endpoint_url = nil, options = {})
|
|
46
162
|
options[:endpoint_url] = endpoint_url
|
|
47
163
|
raise InvalidAPIURL if !endpoint_url.nil? && endpoint_url !~ URI::RFC3986_Parser::RFC3986_URI
|
|
@@ -52,10 +168,31 @@ module ApiAdaptor
|
|
|
52
168
|
self.endpoint = options[:endpoint_url]
|
|
53
169
|
end
|
|
54
170
|
|
|
171
|
+
# Constructs a URL for a given slug with query parameters
|
|
172
|
+
#
|
|
173
|
+
# @param slug [String] The API endpoint slug
|
|
174
|
+
# @param options [Hash] Query parameters to append
|
|
175
|
+
#
|
|
176
|
+
# @return [String] Full URL with .json extension and query string
|
|
177
|
+
#
|
|
178
|
+
# @example
|
|
179
|
+
# url_for_slug("users/123", include: "posts")
|
|
180
|
+
# # => "https://api.example.com/users/123.json?include=posts"
|
|
55
181
|
def url_for_slug(slug, options = {})
|
|
56
182
|
"#{base_url}/#{slug}.json#{query_string(options)}"
|
|
57
183
|
end
|
|
58
184
|
|
|
185
|
+
# Performs a GET request and wraps the response in a ListResponse
|
|
186
|
+
#
|
|
187
|
+
# @param url [String] The URL to request
|
|
188
|
+
#
|
|
189
|
+
# @return [ListResponse] Paginated response wrapper
|
|
190
|
+
#
|
|
191
|
+
# @example
|
|
192
|
+
# list = client.get_list("/posts?page=1")
|
|
193
|
+
# list.results # => Array of items
|
|
194
|
+
# list.current_page # => 1
|
|
195
|
+
# list.total_pages # => 10
|
|
59
196
|
def get_list(url)
|
|
60
197
|
get_json(url) do |r|
|
|
61
198
|
ApiAdaptor::ListResponse.new(r, self)
|
|
@@ -1,25 +1,55 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module ApiAdaptor
|
|
4
|
-
#
|
|
4
|
+
# Base exception class for all ApiAdaptor errors
|
|
5
5
|
class BaseError < StandardError; end
|
|
6
6
|
|
|
7
|
+
# Raised when too many redirects are followed
|
|
8
|
+
#
|
|
9
|
+
# @see JSONClient#max_redirects
|
|
7
10
|
class TooManyRedirects < BaseError; end
|
|
8
11
|
|
|
12
|
+
# Raised when a redirect response is missing the Location header
|
|
9
13
|
class RedirectLocationMissing < BaseError; end
|
|
10
14
|
|
|
15
|
+
# Raised when connection to the endpoint is refused (ECONNREFUSED)
|
|
11
16
|
class EndpointNotFound < BaseError; end
|
|
12
17
|
|
|
18
|
+
# Raised when a request times out
|
|
19
|
+
#
|
|
20
|
+
# @see JSONClient#initialize for timeout configuration
|
|
13
21
|
class TimedOutException < BaseError; end
|
|
14
22
|
|
|
23
|
+
# Raised when an invalid URL is provided
|
|
15
24
|
class InvalidUrl < BaseError; end
|
|
16
25
|
|
|
26
|
+
# Raised when a socket error occurs during the request
|
|
17
27
|
class SocketErrorException < BaseError; end
|
|
18
28
|
|
|
19
|
-
#
|
|
29
|
+
# Base class for all HTTP 4xx and 5xx error responses
|
|
30
|
+
#
|
|
31
|
+
# Provides access to the HTTP status code, error details, and response body.
|
|
32
|
+
#
|
|
33
|
+
# @example Handling HTTP errors
|
|
34
|
+
# begin
|
|
35
|
+
# client.get_json(url)
|
|
36
|
+
# rescue ApiAdaptor::HTTPNotFound => e
|
|
37
|
+
# puts "Resource not found: #{e.code}"
|
|
38
|
+
# rescue ApiAdaptor::HTTPServerError => e
|
|
39
|
+
# puts "Server error: #{e.code} - #{e.error_details}"
|
|
40
|
+
# end
|
|
20
41
|
class HTTPErrorResponse < BaseError
|
|
42
|
+
# @return [Integer] HTTP status code
|
|
43
|
+
# @return [Hash, nil] Parsed error details from response body
|
|
44
|
+
# @return [String, nil] Raw HTTP response body
|
|
21
45
|
attr_accessor :code, :error_details, :http_body
|
|
22
46
|
|
|
47
|
+
# Initializes a new HTTP error response
|
|
48
|
+
#
|
|
49
|
+
# @param code [Integer] HTTP status code
|
|
50
|
+
# @param message [String, nil] Error message
|
|
51
|
+
# @param error_details [Hash, nil] Parsed error details from JSON body
|
|
52
|
+
# @param http_body [String, nil] Raw HTTP response body
|
|
23
53
|
def initialize(code, message = nil, error_details = nil, http_body = nil)
|
|
24
54
|
super(message)
|
|
25
55
|
@code = code
|
|
@@ -28,51 +58,84 @@ module ApiAdaptor
|
|
|
28
58
|
end
|
|
29
59
|
end
|
|
30
60
|
|
|
31
|
-
#
|
|
61
|
+
# Base class for all HTTP 4xx client errors
|
|
32
62
|
class HTTPClientError < HTTPErrorResponse; end
|
|
33
63
|
|
|
64
|
+
# Base class for intermittent client errors that may succeed on retry
|
|
34
65
|
class HTTPIntermittentClientError < HTTPClientError; end
|
|
35
66
|
|
|
67
|
+
# Raised on HTTP 404 Not Found
|
|
36
68
|
class HTTPNotFound < HTTPClientError; end
|
|
37
69
|
|
|
70
|
+
# Raised on HTTP 410 Gone
|
|
38
71
|
class HTTPGone < HTTPClientError; end
|
|
39
72
|
|
|
73
|
+
# Raised on HTTP 413 Payload Too Large
|
|
40
74
|
class HTTPPayloadTooLarge < HTTPClientError; end
|
|
41
75
|
|
|
76
|
+
# Raised on HTTP 401 Unauthorized
|
|
42
77
|
class HTTPUnauthorized < HTTPClientError; end
|
|
43
78
|
|
|
79
|
+
# Raised on HTTP 403 Forbidden
|
|
44
80
|
class HTTPForbidden < HTTPClientError; end
|
|
45
81
|
|
|
82
|
+
# Raised on HTTP 409 Conflict
|
|
46
83
|
class HTTPConflict < HTTPClientError; end
|
|
47
84
|
|
|
85
|
+
# Raised on HTTP 422 Unprocessable Entity
|
|
48
86
|
class HTTPUnprocessableEntity < HTTPClientError; end
|
|
49
87
|
|
|
88
|
+
# Raised on HTTP 422 Unprocessable Content (alternative name)
|
|
50
89
|
class HTTPUnprocessableContent < HTTPClientError; end
|
|
51
90
|
|
|
91
|
+
# Raised on HTTP 400 Bad Request
|
|
52
92
|
class HTTPBadRequest < HTTPClientError; end
|
|
53
93
|
|
|
94
|
+
# Raised on HTTP 429 Too Many Requests
|
|
54
95
|
class HTTPTooManyRequests < HTTPIntermittentClientError; end
|
|
55
96
|
|
|
56
|
-
#
|
|
97
|
+
# Base class for all HTTP 5xx server errors
|
|
57
98
|
class HTTPServerError < HTTPErrorResponse; end
|
|
58
99
|
|
|
100
|
+
# Base class for intermittent server errors that may succeed on retry
|
|
59
101
|
class HTTPIntermittentServerError < HTTPServerError; end
|
|
60
102
|
|
|
103
|
+
# Raised on HTTP 500 Internal Server Error
|
|
61
104
|
class HTTPInternalServerError < HTTPServerError; end
|
|
62
105
|
|
|
106
|
+
# Raised on HTTP 502 Bad Gateway
|
|
63
107
|
class HTTPBadGateway < HTTPIntermittentServerError; end
|
|
64
108
|
|
|
109
|
+
# Raised on HTTP 503 Service Unavailable
|
|
65
110
|
class HTTPUnavailable < HTTPIntermittentServerError; end
|
|
66
111
|
|
|
112
|
+
# Raised on HTTP 504 Gateway Timeout
|
|
67
113
|
class HTTPGatewayTimeout < HTTPIntermittentServerError; end
|
|
68
114
|
|
|
115
|
+
# Module providing HTTP error handling and exception mapping
|
|
69
116
|
module ExceptionHandling
|
|
117
|
+
# Builds a specific HTTP error exception based on the status code
|
|
118
|
+
#
|
|
119
|
+
# @param error [RestClient::Exception] The RestClient exception
|
|
120
|
+
# @param url [String] The URL that was requested
|
|
121
|
+
# @param details [Hash, nil] Parsed error details from JSON response
|
|
122
|
+
#
|
|
123
|
+
# @return [HTTPErrorResponse] Specific exception instance
|
|
124
|
+
#
|
|
125
|
+
# @api private
|
|
70
126
|
def build_specific_http_error(error, url, details = nil)
|
|
71
127
|
message = "URL: #{url}\nResponse body:\n#{error.http_body}"
|
|
72
128
|
code = error.http_code
|
|
73
129
|
error_class_for_code(code).new(code, message, details, error.http_body)
|
|
74
130
|
end
|
|
75
131
|
|
|
132
|
+
# Maps HTTP status codes to exception classes
|
|
133
|
+
#
|
|
134
|
+
# @param code [Integer] HTTP status code
|
|
135
|
+
#
|
|
136
|
+
# @return [Class] Exception class for the status code
|
|
137
|
+
#
|
|
138
|
+
# @api private
|
|
76
139
|
def error_class_for_code(code)
|
|
77
140
|
case code
|
|
78
141
|
when 400
|
data/lib/api_adaptor/headers.rb
CHANGED
|
@@ -1,22 +1,54 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module ApiAdaptor
|
|
4
|
+
# Thread-safe header management for HTTP requests
|
|
5
|
+
#
|
|
6
|
+
# Headers are stored in thread-local storage, allowing different threads
|
|
7
|
+
# to maintain separate header contexts without interference.
|
|
8
|
+
#
|
|
9
|
+
# @example Setting custom headers
|
|
10
|
+
# ApiAdaptor::Headers.set_header("X-Request-ID", "12345")
|
|
11
|
+
# ApiAdaptor::Headers.set_header("X-Correlation-ID", "abcde")
|
|
12
|
+
#
|
|
13
|
+
# @example Getting all headers
|
|
14
|
+
# headers = ApiAdaptor::Headers.headers
|
|
15
|
+
# # => {"X-Request-ID" => "12345", "X-Correlation-ID" => "abcde"}
|
|
16
|
+
#
|
|
17
|
+
# @example Clearing headers
|
|
18
|
+
# ApiAdaptor::Headers.clear_headers
|
|
4
19
|
class Headers
|
|
5
20
|
class << self
|
|
21
|
+
# Sets a header value for the current thread
|
|
22
|
+
#
|
|
23
|
+
# @param header_name [String] Header name
|
|
24
|
+
# @param value [String] Header value
|
|
25
|
+
#
|
|
26
|
+
# @return [String] The value that was set
|
|
6
27
|
def set_header(header_name, value)
|
|
7
28
|
header_data[header_name] = value
|
|
8
29
|
end
|
|
9
30
|
|
|
31
|
+
# Returns all non-empty headers for the current thread
|
|
32
|
+
#
|
|
33
|
+
# @return [Hash] Hash of header names to values, excluding nil/empty values
|
|
10
34
|
def headers
|
|
11
35
|
header_data.reject { |_k, v| v.nil? || v.empty? }
|
|
12
36
|
end
|
|
13
37
|
|
|
38
|
+
# Clears all headers for the current thread
|
|
39
|
+
#
|
|
40
|
+
# @return [Hash] Empty hash
|
|
14
41
|
def clear_headers
|
|
15
42
|
Thread.current[:headers] = {}
|
|
16
43
|
end
|
|
17
44
|
|
|
18
45
|
private
|
|
19
46
|
|
|
47
|
+
# Returns the thread-local header storage
|
|
48
|
+
#
|
|
49
|
+
# @return [Hash] Thread-local header hash
|
|
50
|
+
#
|
|
51
|
+
# @api private
|
|
20
52
|
def header_data
|
|
21
53
|
Thread.current[:headers] ||= {}
|
|
22
54
|
end
|