api_adaptor 0.1.0 → 1.0.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.
@@ -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
- # Abstract error class
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
- # Superclass for all 4XX and 5XX errors
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
- # Superclass & fallback for all 4XX errors
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
- # Superclass & fallback for all 5XX errors
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
@@ -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
@@ -9,11 +9,68 @@ require_relative "response"
9
9
  require "rest-client"
10
10
 
11
11
  module ApiAdaptor
12
+ # HTTP client for JSON APIs with comprehensive redirect handling and authentication support.
13
+ #
14
+ # JSONClient provides a low-level interface for making HTTP requests to JSON APIs. It handles
15
+ # automatic JSON parsing, configurable redirect following, authentication (bearer token and basic auth),
16
+ # timeout management, and comprehensive error handling.
17
+ #
18
+ # @example Basic usage with bearer token
19
+ # client = JSONClient.new(bearer_token: "abc123")
20
+ # response = client.get_json("https://api.example.com/users")
21
+ # users = response["data"]
22
+ #
23
+ # @example Custom timeout and redirect configuration
24
+ # client = JSONClient.new(
25
+ # timeout: 10,
26
+ # max_redirects: 5,
27
+ # follow_non_get_redirects: true
28
+ # )
29
+ #
30
+ # @example With basic authentication
31
+ # client = JSONClient.new(
32
+ # basic_auth: { user: "username", password: "password" }
33
+ # )
34
+ #
35
+ # @example Disable cross-origin redirects for security
36
+ # client = JSONClient.new(
37
+ # allow_cross_origin_redirects: false
38
+ # )
39
+ #
40
+ # @see Base for a higher-level API client framework
12
41
  class JsonClient
13
42
  include ApiAdaptor::ExceptionHandling
14
43
 
44
+ # @return [Logger] Logger instance for request/response logging
45
+ # @return [Hash] Client configuration options
15
46
  attr_accessor :logger, :options
16
47
 
48
+ # Initializes a new JSON HTTP client
49
+ #
50
+ # @param options [Hash] Configuration options
51
+ # @option options [String] :bearer_token Bearer token for Authorization header
52
+ # @option options [Hash] :basic_auth Basic authentication credentials with :user and :password keys
53
+ # @option options [Integer] :timeout Request timeout in seconds (default: 4)
54
+ # @option options [Integer] :max_redirects Maximum number of redirects to follow (default: 3)
55
+ # @option options [Boolean] :allow_cross_origin_redirects Allow redirects to different origins (default: true)
56
+ # @option options [Boolean] :forward_auth_on_cross_origin_redirects Forward auth headers on cross-origin redirects (default: false, security risk if enabled)
57
+ # @option options [Boolean] :follow_non_get_redirects Follow redirects for POST/PUT/PATCH/DELETE (default: false, only 307/308 supported)
58
+ # @option options [Logger] :logger Custom logger instance (default: NullLogger)
59
+ #
60
+ # @raise [RuntimeError] If disable_timeout or negative timeout is provided
61
+ #
62
+ # @example Default configuration
63
+ # client = JSONClient.new
64
+ # # timeout: 4s, max_redirects: 3, only GET/HEAD follow redirects
65
+ #
66
+ # @example Full configuration
67
+ # client = JSONClient.new(
68
+ # bearer_token: "secret",
69
+ # timeout: 10,
70
+ # max_redirects: 5,
71
+ # allow_cross_origin_redirects: false,
72
+ # logger: Logger.new($stdout)
73
+ # )
17
74
  def initialize(options = {})
18
75
  raise "It is no longer possible to disable the timeout." if options[:disable_timeout] || options[:timeout].to_i.negative?
19
76
 
@@ -21,6 +78,9 @@ module ApiAdaptor
21
78
  @options = options
22
79
  end
23
80
 
81
+ # Returns default HTTP headers for all requests
82
+ #
83
+ # @return [Hash] Default headers including Accept and User-Agent
24
84
  def self.default_request_headers
25
85
  {
26
86
  "Accept" => "application/json",
@@ -28,52 +88,161 @@ module ApiAdaptor
28
88
  }
29
89
  end
30
90
 
91
+ # Returns default headers for requests with JSON body
92
+ #
93
+ # @return [Hash] Default headers plus Content-Type: application/json
31
94
  def self.default_request_with_json_body_headers
32
95
  default_request_headers.merge(json_body_headers)
33
96
  end
34
97
 
98
+ # Returns Content-Type header for JSON requests
99
+ #
100
+ # @return [Hash] Content-Type header
35
101
  def self.json_body_headers
36
102
  {
37
103
  "Content-Type" => "application/json"
38
104
  }
39
105
  end
40
106
 
107
+ # Default request timeout in seconds
41
108
  DEFAULT_TIMEOUT_IN_SECONDS = 4
109
+
110
+ # Default maximum number of redirects to follow
42
111
  DEFAULT_MAX_REDIRECTS = 3
43
112
 
113
+ # Performs a GET request and returns the raw response
114
+ #
115
+ # @param url [String] The URL to request
116
+ #
117
+ # @return [RestClient::Response] Raw HTTP response
118
+ #
119
+ # @raise [HTTPClientError, HTTPServerError] On HTTP errors
120
+ # @raise [TimedOutException] On timeout
121
+ # @raise [EndpointNotFound] On connection refused
122
+ # @raise [InvalidUrl] On invalid URI
123
+ # @raise [TooManyRedirects] When max_redirects is exceeded
124
+ # @raise [RedirectLocationMissing] When redirect lacks Location header
44
125
  def get_raw!(url)
45
126
  do_raw_request(:get, url)
46
127
  end
47
128
 
129
+ # Performs a GET request and returns the raw response (alias for get_raw!)
130
+ #
131
+ # @param url [String] The URL to request
132
+ #
133
+ # @return [RestClient::Response] Raw HTTP response
134
+ #
135
+ # @see #get_raw!
48
136
  def get_raw(url)
49
137
  get_raw!(url)
50
138
  end
51
139
 
140
+ # Performs a GET request and parses the JSON response
141
+ #
142
+ # @param url [String] The URL to request
143
+ # @param additional_headers [Hash] Additional HTTP headers to include
144
+ # @yield [response] Optional block to create custom response object
145
+ # @yieldparam response [RestClient::Response] The raw HTTP response
146
+ # @yieldreturn [Object] Custom response object
147
+ #
148
+ # @return [Response, Object] Response object or custom object from block
149
+ #
150
+ # @raise [HTTPClientError, HTTPServerError] On HTTP errors
151
+ # @raise [TimedOutException] On timeout
152
+ #
153
+ # @example Basic usage
154
+ # response = client.get_json("https://api.example.com/users")
155
+ # users = response["data"]
156
+ #
157
+ # @example With custom response class
158
+ # users = client.get_json("https://api.example.com/users") do |r|
159
+ # UserListResponse.new(r)
160
+ # end
52
161
  def get_json(url, additional_headers = {}, &create_response)
53
162
  do_json_request(:get, url, nil, additional_headers, &create_response)
54
163
  end
55
164
 
165
+ # Performs a POST request with JSON body
166
+ #
167
+ # @param url [String] The URL to request
168
+ # @param params [Hash] Data to send as JSON in request body (default: {})
169
+ # @param additional_headers [Hash] Additional HTTP headers to include
170
+ #
171
+ # @return [Response] Response object with parsed JSON
172
+ #
173
+ # @raise [HTTPClientError, HTTPServerError] On HTTP errors
174
+ #
175
+ # @example
176
+ # response = client.post_json("https://api.example.com/users", {
177
+ # name: "Alice",
178
+ # email: "alice@example.com"
179
+ # })
56
180
  def post_json(url, params = {}, additional_headers = {})
57
181
  do_json_request(:post, url, params, additional_headers)
58
182
  end
59
183
 
184
+ # Performs a PUT request with JSON body
185
+ #
186
+ # @param url [String] The URL to request
187
+ # @param params [Hash] Data to send as JSON in request body
188
+ # @param additional_headers [Hash] Additional HTTP headers to include
189
+ #
190
+ # @return [Response] Response object with parsed JSON
191
+ #
192
+ # @raise [HTTPClientError, HTTPServerError] On HTTP errors
60
193
  def put_json(url, params, additional_headers = {})
61
194
  do_json_request(:put, url, params, additional_headers)
62
195
  end
63
196
 
197
+ # Performs a PATCH request with JSON body
198
+ #
199
+ # @param url [String] The URL to request
200
+ # @param params [Hash] Data to send as JSON in request body
201
+ # @param additional_headers [Hash] Additional HTTP headers to include
202
+ #
203
+ # @return [Response] Response object with parsed JSON
204
+ #
205
+ # @raise [HTTPClientError, HTTPServerError] On HTTP errors
64
206
  def patch_json(url, params, additional_headers = {})
65
207
  do_json_request(:patch, url, params, additional_headers)
66
208
  end
67
209
 
210
+ # Performs a DELETE request with optional JSON body
211
+ #
212
+ # @param url [String] The URL to request
213
+ # @param params [Hash] Optional data to send as JSON in request body (default: {})
214
+ # @param additional_headers [Hash] Additional HTTP headers to include
215
+ #
216
+ # @return [Response] Response object with parsed JSON
217
+ #
218
+ # @raise [HTTPClientError, HTTPServerError] On HTTP errors
68
219
  def delete_json(url, params = {}, additional_headers = {})
69
220
  do_json_request(:delete, url, params, additional_headers)
70
221
  end
71
222
 
223
+ # Performs a POST request with multipart/form-data
224
+ #
225
+ # @param url [String] The URL to request
226
+ # @param params [Hash] Multipart form data (may include file uploads)
227
+ #
228
+ # @return [Response] Response object
229
+ #
230
+ # @example Uploading a file
231
+ # client.post_multipart("https://api.example.com/upload", {
232
+ # file: File.open("image.jpg", "rb"),
233
+ # description: "Profile photo"
234
+ # })
72
235
  def post_multipart(url, params)
73
236
  r = do_raw_request(:post, url, params.merge(multipart: true))
74
237
  Response.new(r)
75
238
  end
76
239
 
240
+ # Performs a PUT request with multipart/form-data
241
+ #
242
+ # @param url [String] The URL to request
243
+ # @param params [Hash] Multipart form data (may include file uploads)
244
+ #
245
+ # @return [Response] Response object
77
246
  def put_multipart(url, params)
78
247
  r = do_raw_request(:put, url, params.merge(multipart: true))
79
248
  Response.new(r)
@@ -142,9 +311,9 @@ module ApiAdaptor
142
311
  def with_headers(method_params, default_headers, additional_headers)
143
312
  method_params.merge(
144
313
  headers: default_headers
145
- .merge(method_params[:headers] || {})
146
- .merge(ApiAdaptor::Headers.headers)
147
- .merge(additional_headers)
314
+ .merge(method_params[:headers] || {})
315
+ .merge(ApiAdaptor::Headers.headers)
316
+ .merge(additional_headers)
148
317
  )
149
318
  end
150
319