x 0.17.0 → 0.18.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/README.md +4 -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 +95 -57
  8. data/lib/x/client_credentials.rb +208 -0
  9. data/lib/x/connection.rb +88 -2
  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/version.rb +2 -1
  40. data/sig/x.rbs +78 -17
  41. metadata +6 -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,24 @@ 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
23
+ # Default debug output destination
16
24
  DEFAULT_DEBUG_OUTPUT = File.open(IO::NULL, "w")
25
+ # Network errors that should be wrapped in NetworkError
17
26
  NETWORK_ERRORS = [
18
27
  Errno::ECONNREFUSED,
19
28
  Errno::ECONNRESET,
@@ -22,14 +31,66 @@ module X
22
31
  OpenSSL::SSL::SSLError
23
32
  ].freeze
24
33
 
25
- attr_accessor :open_timeout, :read_timeout, :write_timeout, :debug_output
26
- attr_reader :proxy_url, :proxy_uri
34
+ # The timeout for opening connections in seconds
35
+ # @api public
36
+ # @return [Integer] the timeout for opening connections in seconds
37
+ # @example Get or set the open timeout
38
+ # connection.open_timeout = 30
39
+ attr_accessor :open_timeout
40
+
41
+ # The timeout for reading responses in seconds
42
+ # @api public
43
+ # @return [Integer] the timeout for reading responses in seconds
44
+ # @example Get or set the read timeout
45
+ # connection.read_timeout = 30
46
+ attr_accessor :read_timeout
47
+
48
+ # The timeout for writing requests in seconds
49
+ # @api public
50
+ # @return [Integer] the timeout for writing requests in seconds
51
+ # @example Get or set the write timeout
52
+ # connection.write_timeout = 30
53
+ attr_accessor :write_timeout
54
+
55
+ # The IO object for debug output
56
+ # @api public
57
+ # @return [IO] the IO object for debug output
58
+ # @example Get or set the debug output
59
+ # connection.debug_output = $stderr
60
+ attr_accessor :debug_output
61
+
62
+ # The proxy URL for requests
63
+ # @api public
64
+ # @return [String, nil] the proxy URL for requests
65
+ # @example Get the proxy URL
66
+ # connection.proxy_url
67
+ attr_reader :proxy_url
68
+
69
+ # The parsed proxy URI
70
+ # @api public
71
+ # @return [URI, nil] the parsed proxy URI
72
+ # @example Get the proxy URI
73
+ # connection.proxy_uri
74
+ attr_reader :proxy_uri
27
75
 
28
76
  def_delegator :proxy_uri, :host, :proxy_host
29
77
  def_delegator :proxy_uri, :port, :proxy_port
30
78
  def_delegator :proxy_uri, :user, :proxy_user
31
79
  def_delegator :proxy_uri, :password, :proxy_pass
32
80
 
81
+ # Initialize a new connection
82
+ #
83
+ # @api public
84
+ # @param open_timeout [Integer] the timeout for opening connections in seconds
85
+ # @param read_timeout [Integer] the timeout for reading responses in seconds
86
+ # @param write_timeout [Integer] the timeout for writing requests in seconds
87
+ # @param debug_output [IO] the IO object for debug output
88
+ # @param proxy_url [String, nil] the proxy URL for requests
89
+ # @return [Connection] a new connection instance
90
+ # @example Create a connection with default settings
91
+ # connection = X::Connection.new
92
+ # @example Create a connection with custom timeouts
93
+ # connection = X::Connection.new(open_timeout: 30, read_timeout: 30)
33
94
  def initialize(open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT,
34
95
  write_timeout: DEFAULT_WRITE_TIMEOUT, debug_output: DEFAULT_DEBUG_OUTPUT, proxy_url: nil)
35
96
  @open_timeout = open_timeout
@@ -39,6 +100,14 @@ module X
39
100
  self.proxy_url = proxy_url unless proxy_url.nil?
40
101
  end
41
102
 
103
+ # Perform an HTTP request
104
+ #
105
+ # @api public
106
+ # @param request [Net::HTTPRequest] the HTTP request to perform
107
+ # @return [Net::HTTPResponse] the HTTP response
108
+ # @raise [NetworkError] if a network error occurs
109
+ # @example Perform a request
110
+ # response = connection.perform(request: request)
42
111
  def perform(request:)
43
112
  host = request.uri.host || DEFAULT_HOST
44
113
  port = request.uri.port || DEFAULT_PORT
@@ -49,6 +118,14 @@ module X
49
118
  raise NetworkError, "Network error: #{e}"
50
119
  end
51
120
 
121
+ # Set the proxy URL for requests
122
+ #
123
+ # @api public
124
+ # @param proxy_url [String] the proxy URL
125
+ # @return [void]
126
+ # @raise [ArgumentError] if the proxy URL is invalid
127
+ # @example Set the proxy URL
128
+ # connection.proxy_url = "http://proxy.example.com:8080"
52
129
  def proxy_url=(proxy_url)
53
130
  @proxy_url = proxy_url
54
131
  proxy_uri = URI(proxy_url)
@@ -59,6 +136,11 @@ module X
59
136
 
60
137
  private
61
138
 
139
+ # Build an HTTP client for the given host and port
140
+ # @api private
141
+ # @param host [String] the host to connect to
142
+ # @param port [Integer] the port to connect to
143
+ # @return [Net::HTTP] the HTTP client
62
144
  def build_http_client(host = DEFAULT_HOST, port = DEFAULT_PORT)
63
145
  http_client = if proxy_uri
64
146
  Net::HTTP.new(host, port, proxy_host, proxy_port, proxy_user, proxy_pass)
@@ -68,6 +150,10 @@ module X
68
150
  configure_http_client(http_client)
69
151
  end
70
152
 
153
+ # Configure an HTTP client with timeout settings
154
+ # @api private
155
+ # @param http_client [Net::HTTP] the HTTP client to configure
156
+ # @return [Net::HTTP] the configured HTTP client
71
157
  def configure_http_client(http_client)
72
158
  http_client.tap do |c|
73
159
  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
@@ -1,13 +1,32 @@
1
1
  module X
2
+ # Validates media upload parameters
3
+ # @api public
2
4
  module MediaUploadValidator
3
5
  module_function
4
6
 
7
+ # Valid media category values
5
8
  MEDIA_CATEGORIES = %w[dm_gif dm_image dm_video subtitles tweet_gif tweet_image tweet_video].freeze
6
9
 
10
+ # Validate that a file path exists
11
+ #
12
+ # @api private
13
+ # @param file_path [String] the file path to validate
14
+ # @return [void]
15
+ # @raise [RuntimeError] if the file does not exist
16
+ # @example Validate a file path
17
+ # MediaUploadValidator.validate_file_path!(file_path: "image.png")
7
18
  def validate_file_path!(file_path:)
8
19
  raise "File not found: #{file_path}" unless File.exist?(file_path)
9
20
  end
10
21
 
22
+ # Validate that a media category is valid
23
+ #
24
+ # @api private
25
+ # @param media_category [String] the media category to validate
26
+ # @return [void]
27
+ # @raise [ArgumentError] if the media category is invalid
28
+ # @example Validate a media category
29
+ # MediaUploadValidator.validate_media_category!(media_category: "tweet_image")
11
30
  def validate_media_category!(media_category:)
12
31
  return if MEDIA_CATEGORIES.include?(media_category.downcase)
13
32