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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -3
- data/.yardopts +10 -0
- data/CHANGELOG.md +29 -1
- data/CLAUDE.md +423 -0
- data/Gemfile.lock +12 -3
- 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 +45 -1
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
|
|
@@ -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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
314
|
+
.merge(method_params[:headers] || {})
|
|
315
|
+
.merge(ApiAdaptor::Headers.headers)
|
|
316
|
+
.merge(additional_headers)
|
|
148
317
|
)
|
|
149
318
|
end
|
|
150
319
|
|