x 0.17.0 → 0.19.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/README.md +17 -4
  4. data/lib/x/account_uploader.rb +168 -0
  5. data/lib/x/authenticator.rb +12 -0
  6. data/lib/x/bearer_token_authenticator.rb +22 -1
  7. data/lib/x/client.rb +121 -52
  8. data/lib/x/client_credentials.rb +208 -0
  9. data/lib/x/connection.rb +107 -4
  10. data/lib/x/errors/bad_gateway.rb +1 -0
  11. data/lib/x/errors/bad_request.rb +1 -0
  12. data/lib/x/errors/client_error.rb +1 -0
  13. data/lib/x/errors/connection_exception.rb +1 -0
  14. data/lib/x/errors/error.rb +1 -0
  15. data/lib/x/errors/forbidden.rb +1 -0
  16. data/lib/x/errors/gateway_timeout.rb +1 -0
  17. data/lib/x/errors/gone.rb +1 -0
  18. data/lib/x/errors/http_error.rb +47 -4
  19. data/lib/x/errors/internal_server_error.rb +1 -0
  20. data/lib/x/errors/invalid_media_type.rb +6 -0
  21. data/lib/x/errors/network_error.rb +1 -0
  22. data/lib/x/errors/not_acceptable.rb +1 -0
  23. data/lib/x/errors/not_found.rb +1 -0
  24. data/lib/x/errors/payload_too_large.rb +1 -0
  25. data/lib/x/errors/server_error.rb +1 -0
  26. data/lib/x/errors/service_unavailable.rb +1 -0
  27. data/lib/x/errors/too_many_redirects.rb +1 -0
  28. data/lib/x/errors/too_many_requests.rb +32 -0
  29. data/lib/x/errors/unauthorized.rb +1 -0
  30. data/lib/x/errors/unprocessable_entity.rb +1 -0
  31. data/lib/x/media_upload_validator.rb +19 -0
  32. data/lib/x/media_uploader.rb +117 -5
  33. data/lib/x/oauth2_authenticator.rb +169 -0
  34. data/lib/x/oauth_authenticator.rb +99 -2
  35. data/lib/x/rate_limit.rb +57 -1
  36. data/lib/x/redirect_handler.rb +55 -1
  37. data/lib/x/request_builder.rb +36 -0
  38. data/lib/x/response_parser.rb +21 -0
  39. data/lib/x/stream_parser.rb +75 -0
  40. data/lib/x/version.rb +2 -1
  41. data/sig/x.rbs +94 -18
  42. metadata +7 -2
@@ -0,0 +1,208 @@
1
+ module X
2
+ # Mixin for client authentication credentials
3
+ # @api private
4
+ module ClientCredentials
5
+ # The API key for OAuth 1.0a authentication
6
+ # @api public
7
+ # @return [String, nil] the API key for OAuth 1.0a authentication
8
+ # @example Get the API key
9
+ # client.api_key
10
+ attr_reader :api_key
11
+
12
+ # The API key secret for OAuth 1.0a authentication
13
+ # @api public
14
+ # @return [String, nil] the API key secret for OAuth 1.0a authentication
15
+ # @example Get the API key secret
16
+ # client.api_key_secret
17
+ attr_reader :api_key_secret
18
+
19
+ # The access token for OAuth authentication
20
+ # @api public
21
+ # @return [String, nil] the access token for OAuth authentication
22
+ # @example Get the access token
23
+ # client.access_token
24
+ attr_reader :access_token
25
+
26
+ # The access token secret for OAuth 1.0a authentication
27
+ # @api public
28
+ # @return [String, nil] the access token secret for OAuth 1.0a authentication
29
+ # @example Get the access token secret
30
+ # client.access_token_secret
31
+ attr_reader :access_token_secret
32
+
33
+ # The bearer token for authentication
34
+ # @api public
35
+ # @return [String, nil] the bearer token for authentication
36
+ # @example Get the bearer token
37
+ # client.bearer_token
38
+ attr_reader :bearer_token
39
+
40
+ # The OAuth 2.0 client ID
41
+ # @api public
42
+ # @return [String, nil] the OAuth 2.0 client ID
43
+ # @example Get the client ID
44
+ # client.client_id
45
+ attr_reader :client_id
46
+
47
+ # The OAuth 2.0 client secret
48
+ # @api public
49
+ # @return [String, nil] the OAuth 2.0 client secret
50
+ # @example Get the client secret
51
+ # client.client_secret
52
+ attr_reader :client_secret
53
+
54
+ # The OAuth 2.0 refresh token
55
+ # @api public
56
+ # @return [String, nil] the OAuth 2.0 refresh token
57
+ # @example Get the refresh token
58
+ # client.refresh_token
59
+ attr_reader :refresh_token
60
+
61
+ # Set the API key for OAuth 1.0a authentication
62
+ #
63
+ # @api public
64
+ # @param api_key [String] the API key for OAuth 1.0a authentication
65
+ # @return [void]
66
+ # @example Set the API key
67
+ # client.api_key = "new_key"
68
+ def api_key=(api_key)
69
+ @api_key = api_key
70
+ initialize_authenticator
71
+ end
72
+
73
+ # Set the API key secret for OAuth 1.0a authentication
74
+ #
75
+ # @api public
76
+ # @param api_key_secret [String] the API key secret for OAuth 1.0a authentication
77
+ # @return [void]
78
+ # @example Set the API key secret
79
+ # client.api_key_secret = "new_secret"
80
+ def api_key_secret=(api_key_secret)
81
+ @api_key_secret = api_key_secret
82
+ initialize_authenticator
83
+ end
84
+
85
+ # Set the access token for OAuth authentication
86
+ #
87
+ # @api public
88
+ # @param access_token [String] the access token for OAuth authentication
89
+ # @return [void]
90
+ # @example Set the access token
91
+ # client.access_token = "new_token"
92
+ def access_token=(access_token)
93
+ @access_token = access_token
94
+ initialize_authenticator
95
+ end
96
+
97
+ # Set the access token secret for OAuth 1.0a authentication
98
+ #
99
+ # @api public
100
+ # @param access_token_secret [String] the access token secret for OAuth 1.0a authentication
101
+ # @return [void]
102
+ # @example Set the access token secret
103
+ # client.access_token_secret = "new_secret"
104
+ def access_token_secret=(access_token_secret)
105
+ @access_token_secret = access_token_secret
106
+ initialize_authenticator
107
+ end
108
+
109
+ # Set the bearer token for authentication
110
+ #
111
+ # @api public
112
+ # @param bearer_token [String] the bearer token for authentication
113
+ # @return [void]
114
+ # @example Set the bearer token
115
+ # client.bearer_token = "new_token"
116
+ def bearer_token=(bearer_token)
117
+ @bearer_token = bearer_token
118
+ initialize_authenticator
119
+ end
120
+
121
+ # Set the OAuth 2.0 client ID
122
+ #
123
+ # @api public
124
+ # @param client_id [String] the OAuth 2.0 client ID
125
+ # @return [void]
126
+ # @example Set the client ID
127
+ # client.client_id = "new_id"
128
+ def client_id=(client_id)
129
+ @client_id = client_id
130
+ initialize_authenticator
131
+ end
132
+
133
+ # Set the OAuth 2.0 client secret
134
+ #
135
+ # @api public
136
+ # @param client_secret [String] the OAuth 2.0 client secret
137
+ # @return [void]
138
+ # @example Set the client secret
139
+ # client.client_secret = "new_secret"
140
+ def client_secret=(client_secret)
141
+ @client_secret = client_secret
142
+ initialize_authenticator
143
+ end
144
+
145
+ # Set the OAuth 2.0 refresh token
146
+ #
147
+ # @api public
148
+ # @param refresh_token [String] the OAuth 2.0 refresh token
149
+ # @return [void]
150
+ # @example Set the refresh token
151
+ # client.refresh_token = "new_token"
152
+ def refresh_token=(refresh_token)
153
+ @refresh_token = refresh_token
154
+ initialize_authenticator
155
+ end
156
+
157
+ private
158
+
159
+ # Initialize credential instance variables
160
+ # @api private
161
+ # @return [void]
162
+ def initialize_credentials(api_key:, api_key_secret:, access_token:, access_token_secret:, bearer_token:,
163
+ client_id:, client_secret:, refresh_token:)
164
+ @api_key = api_key
165
+ @api_key_secret = api_key_secret
166
+ @access_token = access_token
167
+ @access_token_secret = access_token_secret
168
+ @bearer_token = bearer_token
169
+ @client_id = client_id
170
+ @client_secret = client_secret
171
+ @refresh_token = refresh_token
172
+ end
173
+
174
+ # Initialize the appropriate authenticator based on available credentials
175
+ # @api private
176
+ # @return [Authenticator] the initialized authenticator
177
+ def initialize_authenticator
178
+ @authenticator = oauth_authenticator || oauth2_authenticator || bearer_authenticator || @authenticator || Authenticator.new
179
+ end
180
+
181
+ # Build an OAuth 1.0a authenticator if credentials are available
182
+ # @api private
183
+ # @return [OAuthAuthenticator, nil] the OAuth authenticator or nil
184
+ def oauth_authenticator
185
+ return unless api_key && api_key_secret && access_token && access_token_secret
186
+
187
+ OAuthAuthenticator.new(api_key:, api_key_secret:, access_token:, access_token_secret:)
188
+ end
189
+
190
+ # Build an OAuth 2.0 authenticator if credentials are available
191
+ # @api private
192
+ # @return [OAuth2Authenticator, nil] the OAuth 2.0 authenticator or nil
193
+ def oauth2_authenticator
194
+ return unless client_id && client_secret && access_token && refresh_token
195
+
196
+ OAuth2Authenticator.new(client_id:, client_secret:, access_token:, refresh_token:)
197
+ end
198
+
199
+ # Build a bearer token authenticator if credentials are available
200
+ # @api private
201
+ # @return [BearerTokenAuthenticator, nil] the bearer token authenticator or nil
202
+ def bearer_authenticator
203
+ return unless bearer_token
204
+
205
+ BearerTokenAuthenticator.new(bearer_token:)
206
+ end
207
+ end
208
+ end
data/lib/x/connection.rb CHANGED
@@ -5,15 +5,22 @@ require "uri"
5
5
  require_relative "errors/network_error"
6
6
 
7
7
  module X
8
+ # Manages HTTP connections to the X API
9
+ # @api public
8
10
  class Connection
9
11
  extend Forwardable
10
12
 
13
+ # Default host for the X API
11
14
  DEFAULT_HOST = "api.twitter.com".freeze
15
+ # Default port for HTTPS connections
12
16
  DEFAULT_PORT = 443
17
+ # Default timeout for opening connections in seconds
13
18
  DEFAULT_OPEN_TIMEOUT = 60 # seconds
19
+ # Default timeout for reading responses in seconds
14
20
  DEFAULT_READ_TIMEOUT = 60 # seconds
21
+ # Default timeout for writing requests in seconds
15
22
  DEFAULT_WRITE_TIMEOUT = 60 # seconds
16
- DEFAULT_DEBUG_OUTPUT = File.open(IO::NULL, "w")
23
+ # Network errors that should be wrapped in NetworkError
17
24
  NETWORK_ERRORS = [
18
25
  Errno::ECONNREFUSED,
19
26
  Errno::ECONNRESET,
@@ -22,16 +29,68 @@ module X
22
29
  OpenSSL::SSL::SSLError
23
30
  ].freeze
24
31
 
25
- attr_accessor :open_timeout, :read_timeout, :write_timeout, :debug_output
26
- attr_reader :proxy_url, :proxy_uri
32
+ # The timeout for opening connections in seconds
33
+ # @api public
34
+ # @return [Integer] the timeout for opening connections in seconds
35
+ # @example Get or set the open timeout
36
+ # connection.open_timeout = 30
37
+ attr_accessor :open_timeout
38
+
39
+ # The timeout for reading responses in seconds
40
+ # @api public
41
+ # @return [Integer] the timeout for reading responses in seconds
42
+ # @example Get or set the read timeout
43
+ # connection.read_timeout = 30
44
+ attr_accessor :read_timeout
45
+
46
+ # The timeout for writing requests in seconds
47
+ # @api public
48
+ # @return [Integer] the timeout for writing requests in seconds
49
+ # @example Get or set the write timeout
50
+ # connection.write_timeout = 30
51
+ attr_accessor :write_timeout
52
+
53
+ # The IO object for debug output
54
+ # @api public
55
+ # @return [IO] the IO object for debug output
56
+ # @example Get or set the debug output
57
+ # connection.debug_output = $stderr
58
+ attr_accessor :debug_output
59
+
60
+ # The proxy URL for requests
61
+ # @api public
62
+ # @return [String, nil] the proxy URL for requests
63
+ # @example Get the proxy URL
64
+ # connection.proxy_url
65
+ attr_reader :proxy_url
66
+
67
+ # The parsed proxy URI
68
+ # @api public
69
+ # @return [URI, nil] the parsed proxy URI
70
+ # @example Get the proxy URI
71
+ # connection.proxy_uri
72
+ attr_reader :proxy_uri
27
73
 
28
74
  def_delegator :proxy_uri, :host, :proxy_host
29
75
  def_delegator :proxy_uri, :port, :proxy_port
30
76
  def_delegator :proxy_uri, :user, :proxy_user
31
77
  def_delegator :proxy_uri, :password, :proxy_pass
32
78
 
79
+ # Initialize a new connection
80
+ #
81
+ # @api public
82
+ # @param open_timeout [Integer] the timeout for opening connections in seconds
83
+ # @param read_timeout [Integer] the timeout for reading responses in seconds
84
+ # @param write_timeout [Integer] the timeout for writing requests in seconds
85
+ # @param debug_output [IO] the IO object for debug output
86
+ # @param proxy_url [String, nil] the proxy URL for requests
87
+ # @return [Connection] a new connection instance
88
+ # @example Create a connection with default settings
89
+ # connection = X::Connection.new
90
+ # @example Create a connection with custom timeouts
91
+ # connection = X::Connection.new(open_timeout: 30, read_timeout: 30)
33
92
  def initialize(open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT,
34
- write_timeout: DEFAULT_WRITE_TIMEOUT, debug_output: DEFAULT_DEBUG_OUTPUT, proxy_url: nil)
93
+ write_timeout: DEFAULT_WRITE_TIMEOUT, debug_output: nil, proxy_url: nil)
35
94
  @open_timeout = open_timeout
36
95
  @read_timeout = read_timeout
37
96
  @write_timeout = write_timeout
@@ -39,6 +98,14 @@ module X
39
98
  self.proxy_url = proxy_url unless proxy_url.nil?
40
99
  end
41
100
 
101
+ # Perform an HTTP request
102
+ #
103
+ # @api public
104
+ # @param request [Net::HTTPRequest] the HTTP request to perform
105
+ # @return [Net::HTTPResponse] the HTTP response
106
+ # @raise [NetworkError] if a network error occurs
107
+ # @example Perform a request
108
+ # response = connection.perform(request: request)
42
109
  def perform(request:)
43
110
  host = request.uri.host || DEFAULT_HOST
44
111
  port = request.uri.port || DEFAULT_PORT
@@ -49,6 +116,33 @@ module X
49
116
  raise NetworkError, "Network error: #{e}"
50
117
  end
51
118
 
119
+ # Perform a streaming HTTP request
120
+ #
121
+ # @api public
122
+ # @param request [Net::HTTPRequest] the HTTP request to perform
123
+ # @yield [Net::HTTPResponse] the HTTP response for streaming
124
+ # @return [void]
125
+ # @raise [NetworkError] if a network error occurs
126
+ # @example Perform a streaming request
127
+ # connection.perform_stream(request: request) { |response| response.read_body { |chunk| } }
128
+ def perform_stream(request:, &)
129
+ host = request.uri.host || DEFAULT_HOST
130
+ port = request.uri.port || DEFAULT_PORT
131
+ http_client = build_http_client(host, port)
132
+ http_client.use_ssl = request.uri.scheme.eql?("https")
133
+ http_client.request(request, &)
134
+ rescue *NETWORK_ERRORS => e
135
+ raise NetworkError, "Network error: #{e}"
136
+ end
137
+
138
+ # Set the proxy URL for requests
139
+ #
140
+ # @api public
141
+ # @param proxy_url [String] the proxy URL
142
+ # @return [void]
143
+ # @raise [ArgumentError] if the proxy URL is invalid
144
+ # @example Set the proxy URL
145
+ # connection.proxy_url = "http://proxy.example.com:8080"
52
146
  def proxy_url=(proxy_url)
53
147
  @proxy_url = proxy_url
54
148
  proxy_uri = URI(proxy_url)
@@ -59,6 +153,11 @@ module X
59
153
 
60
154
  private
61
155
 
156
+ # Build an HTTP client for the given host and port
157
+ # @api private
158
+ # @param host [String] the host to connect to
159
+ # @param port [Integer] the port to connect to
160
+ # @return [Net::HTTP] the HTTP client
62
161
  def build_http_client(host = DEFAULT_HOST, port = DEFAULT_PORT)
63
162
  http_client = if proxy_uri
64
163
  Net::HTTP.new(host, port, proxy_host, proxy_port, proxy_user, proxy_pass)
@@ -68,6 +167,10 @@ module X
68
167
  configure_http_client(http_client)
69
168
  end
70
169
 
170
+ # Configure an HTTP client with timeout settings
171
+ # @api private
172
+ # @param http_client [Net::HTTP] the HTTP client to configure
173
+ # @return [Net::HTTP] the configured HTTP client
71
174
  def configure_http_client(http_client)
72
175
  http_client.tap do |c|
73
176
  c.open_timeout = open_timeout
@@ -1,5 +1,6 @@
1
1
  require_relative "server_error"
2
2
 
3
3
  module X
4
+ # Error raised for HTTP 502 Bad Gateway responses
4
5
  class BadGateway < ServerError; end
5
6
  end
@@ -1,5 +1,6 @@
1
1
  require_relative "client_error"
2
2
 
3
3
  module X
4
+ # Error raised for HTTP 400 Bad Request responses
4
5
  class BadRequest < ClientError; end
5
6
  end
@@ -1,5 +1,6 @@
1
1
  require_relative "http_error"
2
2
 
3
3
  module X
4
+ # Base class for client errors (4xx HTTP status codes)
4
5
  class ClientError < HTTPError; end
5
6
  end
@@ -1,5 +1,6 @@
1
1
  require_relative "client_error"
2
2
 
3
3
  module X
4
+ # Error raised for HTTP 409 Conflict responses
4
5
  class ConnectionException < ClientError; end
5
6
  end
@@ -1,3 +1,4 @@
1
1
  module X
2
+ # Base error class for all X API errors
2
3
  class Error < StandardError; end
3
4
  end
@@ -1,5 +1,6 @@
1
1
  require_relative "client_error"
2
2
 
3
3
  module X
4
+ # Error raised for HTTP 403 Forbidden responses
4
5
  class Forbidden < ClientError; end
5
6
  end
@@ -1,5 +1,6 @@
1
1
  require_relative "server_error"
2
2
 
3
3
  module X
4
+ # Error raised for HTTP 504 Gateway Timeout responses
4
5
  class GatewayTimeout < ServerError; end
5
6
  end
data/lib/x/errors/gone.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require_relative "client_error"
2
2
 
3
3
  module X
4
+ # Error raised for HTTP 410 Gone responses
4
5
  class Gone < ClientError; end
5
6
  end
@@ -2,17 +2,46 @@ require "json"
2
2
  require_relative "error"
3
3
 
4
4
  module X
5
+ # Base class for HTTP errors from the X API
6
+ # @api public
5
7
  class HTTPError < Error
8
+ # Regular expression to match JSON content types
6
9
  JSON_CONTENT_TYPE_REGEXP = %r{application/(problem\+|)json}
7
10
 
8
- attr_reader :response, :code
11
+ # The HTTP response
12
+ # @api public
13
+ # @return [Net::HTTPResponse] the HTTP response
14
+ # @example Get the response
15
+ # error.response
16
+ attr_reader :response
9
17
 
18
+ # The HTTP status code
19
+ # @api public
20
+ # @return [String] the HTTP status code
21
+ # @example Get the status code
22
+ # error.code
23
+ attr_reader :code
24
+
25
+ # Initialize a new HTTPError
26
+ #
27
+ # @api public
28
+ # @param response [Net::HTTPResponse] the HTTP response
29
+ # @return [HTTPError] a new instance
30
+ # @example Create an HTTP error
31
+ # error = X::HTTPError.new(response: response)
10
32
  def initialize(response:)
11
33
  super(error_message(response))
12
34
  @response = response
13
35
  @code = response.code
14
36
  end
15
37
 
38
+ # Get the error message from the response
39
+ #
40
+ # @api public
41
+ # @param response [Net::HTTPResponse] the HTTP response
42
+ # @return [String] the error message
43
+ # @example Get the error message
44
+ # error.error_message(response)
16
45
  def error_message(response)
17
46
  if json?(response)
18
47
  message_from_json_response(response)
@@ -21,19 +50,33 @@ module X
21
50
  end
22
51
  end
23
52
 
53
+ # Extract error message from a JSON response
54
+ #
55
+ # @api public
56
+ # @param response [Net::HTTPResponse] the HTTP response
57
+ # @return [String] the error message
58
+ # @example Get error message from JSON
59
+ # error.message_from_json_response(response)
24
60
  def message_from_json_response(response)
25
61
  response_object = JSON.parse(response.body)
26
- if response_object.key?("title") && response_object.key?("detail")
62
+ if response_object["errors"].instance_of?(Array)
63
+ response_object.fetch("errors").map { |error| error.fetch("message") }.join(", ")
64
+ elsif response_object.key?("title") && response_object.key?("detail")
27
65
  "#{response_object.fetch("title")}: #{response_object.fetch("detail")}"
28
66
  elsif response_object.key?("error")
29
67
  response_object.fetch("error")
30
- elsif response_object["errors"].instance_of?(Array)
31
- response_object.fetch("errors").map { |error| error.fetch("message") }.join(", ")
32
68
  else
33
69
  response.message
34
70
  end
35
71
  end
36
72
 
73
+ # Check if the response contains JSON
74
+ #
75
+ # @api public
76
+ # @param response [Net::HTTPResponse] the HTTP response
77
+ # @return [Boolean] true if the response is JSON
78
+ # @example Check if response is JSON
79
+ # error.json?(response)
37
80
  def json?(response)
38
81
  JSON_CONTENT_TYPE_REGEXP === response["content-type"]
39
82
  end
@@ -1,5 +1,6 @@
1
1
  require_relative "server_error"
2
2
 
3
3
  module X
4
+ # Error raised for HTTP 500 Internal Server Error responses
4
5
  class InternalServerError < ServerError; end
5
6
  end
@@ -0,0 +1,6 @@
1
+ require_relative "error"
2
+
3
+ module X
4
+ # Error raised when a file's MIME type cannot be determined or is unsupported
5
+ class InvalidMediaType < Error; end
6
+ end
@@ -1,5 +1,6 @@
1
1
  require_relative "error"
2
2
 
3
3
  module X
4
+ # Error raised when a network error occurs
4
5
  class NetworkError < Error; end
5
6
  end
@@ -1,5 +1,6 @@
1
1
  require_relative "client_error"
2
2
 
3
3
  module X
4
+ # Error raised for HTTP 406 Not Acceptable responses
4
5
  class NotAcceptable < ClientError; end
5
6
  end
@@ -1,5 +1,6 @@
1
1
  require_relative "client_error"
2
2
 
3
3
  module X
4
+ # Error raised for HTTP 404 Not Found responses
4
5
  class NotFound < ClientError; end
5
6
  end
@@ -1,5 +1,6 @@
1
1
  require_relative "client_error"
2
2
 
3
3
  module X
4
+ # Error raised for HTTP 413 Payload Too Large responses
4
5
  class PayloadTooLarge < ClientError; end
5
6
  end
@@ -1,5 +1,6 @@
1
1
  require_relative "http_error"
2
2
 
3
3
  module X
4
+ # Base class for server errors (5xx HTTP status codes)
4
5
  class ServerError < HTTPError; end
5
6
  end
@@ -1,5 +1,6 @@
1
1
  require_relative "server_error"
2
2
 
3
3
  module X
4
+ # Error raised for HTTP 503 Service Unavailable responses
4
5
  class ServiceUnavailable < ServerError; end
5
6
  end
@@ -1,5 +1,6 @@
1
1
  require_relative "error"
2
2
 
3
3
  module X
4
+ # Error raised when too many redirects are encountered
4
5
  class TooManyRedirects < Error; end
5
6
  end
@@ -2,25 +2,57 @@ require_relative "client_error"
2
2
  require_relative "../rate_limit"
3
3
 
4
4
  module X
5
+ # Error raised when rate limit is exceeded (HTTP 429)
6
+ # @api public
5
7
  class TooManyRequests < ClientError
8
+ # Get the most restrictive rate limit
9
+ #
10
+ # @api public
11
+ # @return [RateLimit, nil] the rate limit with the latest reset time
12
+ # @example Get the rate limit
13
+ # error.rate_limit
6
14
  def rate_limit
7
15
  rate_limits.max_by(&:reset_at)
8
16
  end
9
17
 
18
+ # Get all rate limits from the response
19
+ #
20
+ # @api public
21
+ # @return [Array<RateLimit>] the rate limits that are exhausted
22
+ # @example Get all rate limits
23
+ # error.rate_limits
10
24
  def rate_limits
11
25
  @rate_limits ||= RateLimit::TYPES.filter_map do |type|
12
26
  RateLimit.new(type:, response:) if response["x-#{type}-remaining"].eql?("0")
13
27
  end
14
28
  end
15
29
 
30
+ # Get the time when the rate limit resets
31
+ #
32
+ # @api public
33
+ # @return [Time] the reset time
34
+ # @example Get the reset time
35
+ # error.reset_at
16
36
  def reset_at
17
37
  rate_limit&.reset_at || Time.at(0)
18
38
  end
19
39
 
40
+ # Get the seconds until the rate limit resets
41
+ #
42
+ # @api public
43
+ # @return [Integer] the seconds until reset
44
+ # @example Get the time until reset
45
+ # error.reset_in
20
46
  def reset_in
21
47
  [(reset_at - Time.now).ceil, 0].max
22
48
  end
23
49
 
50
+ # @!method retry_after
51
+ # Alias for reset_in, returns the seconds to wait before retrying
52
+ # @api public
53
+ # @return [Integer] the seconds to wait before retrying
54
+ # @example Get the retry delay
55
+ # error.retry_after
24
56
  alias_method :retry_after, :reset_in
25
57
  end
26
58
  end
@@ -1,5 +1,6 @@
1
1
  require_relative "client_error"
2
2
 
3
3
  module X
4
+ # Error raised for HTTP 401 Unauthorized responses
4
5
  class Unauthorized < ClientError; end
5
6
  end
@@ -1,5 +1,6 @@
1
1
  require_relative "client_error"
2
2
 
3
3
  module X
4
+ # Error raised for HTTP 422 Unprocessable Entity responses
4
5
  class UnprocessableEntity < ClientError; end
5
6
  end