x 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 56ad55c544f1c7774395d906e0d9d07b6dc2a6d20e37d7df0b3e5b9f7b7b2bb6
4
- data.tar.gz: 1747e256413ee2aeae02d8ed0e351b6d411217b00e634a42aeec5299764d6fac
3
+ metadata.gz: ff49ecaad847aef0d9da4bdfc4f89cc3bc54da5edbadab40be265e9ec2ec1cf6
4
+ data.tar.gz: 1ddef9c1479654f53ed7a889cb33b3d1e4d972c005c4f631726bb4270721c2b0
5
5
  SHA512:
6
- metadata.gz: c5b386cd7386457b148c555359e68c4f9cb72b598ff9eb0a75405f3af75599dca0d5fc1700b01653465b2e50e401b69e9f9e5e9dd126d6a56c46b6df96b5736c
7
- data.tar.gz: 462957aeee740d46f4a83497e69bec852fa0a4d8a9e38aec6bf8ffcd8f432e862737fa34847995dce5a49c09a40d9a08607ace68a1314a32972292d881cbd1db
6
+ metadata.gz: 406e9aaa6f6166f9d267718ac22e94324b243c85475b30cb7569db957683014644542a3b44a6a3d337f0658a1204e8656b1b0c4ed2355b12ffc195c34b0734e4
7
+ data.tar.gz: b0754554e6272ef9b9ede3f650c3c407b71ae6ed8ede70d6da314ca408874898e83c2d6c14b31335817b0fb036422adde8ccefe3c13c274794c2e08229196812
data/.rubocop.yml CHANGED
@@ -1,4 +1,6 @@
1
1
  require:
2
+ - standard
3
+ - standard-performance
2
4
  - rubocop-minitest
3
5
  - rubocop-performance
4
6
  - rubocop-rake
@@ -7,6 +9,39 @@ AllCops:
7
9
  NewCops: enable
8
10
  TargetRubyVersion: 3.0
9
11
 
12
+ Layout/ArgumentAlignment:
13
+ Enabled: true
14
+ EnforcedStyle: with_fixed_indentation
15
+
16
+ Layout/ArrayAlignment:
17
+ Enabled: true
18
+ EnforcedStyle: with_fixed_indentation
19
+
20
+ Layout/EndAlignment:
21
+ Enabled: true
22
+ EnforcedStyleAlignWith: variable
23
+
24
+ Layout/HashAlignment:
25
+ Enabled: true
26
+ EnforcedHashRocketStyle: key
27
+ EnforcedColonStyle: key
28
+ EnforcedLastArgumentHashStyle: always_inspect
29
+
30
+ Layout/ParameterAlignment:
31
+ Enabled: true
32
+ EnforcedStyle: with_fixed_indentation
33
+ IndentationWidth: ~
34
+
35
+ Layout/SpaceInsideHashLiteralBraces:
36
+ Enabled: false
37
+
38
+ Metrics/ParameterLists:
39
+ CountKeywordArgs: false
40
+
41
+ Style/Alias:
42
+ Enabled: true
43
+ EnforcedStyle: prefer_alias_method
44
+
10
45
  Style/StringLiterals:
11
46
  Enabled: true
12
47
  EnforcedStyle: double_quotes
@@ -17,6 +52,3 @@ Style/StringLiteralsInInterpolation:
17
52
 
18
53
  Style/FrozenStringLiteralComment:
19
54
  Enabled: false
20
-
21
- Metrics/ParameterLists:
22
- CountKeywordArgs: false
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.0] - 2023-08-10
4
+
5
+ - Add configurable write timeout (2a31f84)
6
+ - Use built-in Gem::Version class (066e0b6)
7
+
8
+ ## [0.4.0] - 2023-08-06
9
+
10
+ - Refactor Client into Authenticator, RequestBuilder, Connection, ResponseHandler (6bee1e9)
11
+ - Add configurable open timeout (1000f9d)
12
+ - Allow configuration of content type (f33a732)
13
+
14
+ ## [0.3.0] - 2023-08-04
15
+
3
16
  - Add accessors to X::Client (e61fa73)
4
17
  - Add configurable read timeout (41502b9)
5
18
  - Handle network-related errors (9ed1fb4)
data/Gemfile CHANGED
@@ -3,6 +3,7 @@ source "https://rubygems.org"
3
3
  # Specify your gem's dependencies in x.gemspec
4
4
  gemspec
5
5
 
6
+ gem "hashie", ">= 5"
6
7
  gem "minitest", ">= 5.19"
7
8
  gem "rake", ">= 13.0.6"
8
9
  gem "rubocop", ">= 1.21"
@@ -10,4 +11,5 @@ gem "rubocop-minitest", ">= 0.31"
10
11
  gem "rubocop-performance", ">= 1.18"
11
12
  gem "rubocop-rake", ">= 0.6"
12
13
  gem "simplecov", ">= 0.22"
14
+ gem "standard", ">= 1.30.1"
13
15
  gem "webmock", ">= 3.18.1"
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # X
2
2
 
3
- A Ruby interface to the X 2.0 API.
3
+ A Ruby interface to the X API.
4
4
 
5
5
  ## Installation
6
6
 
@@ -15,7 +15,7 @@ If bundler is not being used to manage dependencies, install the gem by executin
15
15
  ## Usage
16
16
 
17
17
  ```ruby
18
- oauth_credentials = {
18
+ x_oauth_credentials = {
19
19
  api_key: "INSERT YOUR X API KEY HERE",
20
20
  api_key_secret: "INSERT YOUR X API KEY SECRET HERE",
21
21
  access_token: "INSERT YOUR X API ACCESS TOKEN HERE",
@@ -23,7 +23,7 @@ oauth_credentials = {
23
23
  }
24
24
 
25
25
  # Initialize X API client with OAuth credentials
26
- x_client = X::Client.new(**oauth_credentials)
26
+ x_client = X::Client.new(**x_oauth_credentials)
27
27
 
28
28
  # Request yourself
29
29
  x_client.get("users/me")
@@ -38,18 +38,34 @@ x_client.delete("tweets/#{tweet["data"]["id"]}")
38
38
  # {"data"=>{"deleted"=>true}}
39
39
 
40
40
  # Initialize an API v1.1 client
41
- v1_client = X::Client.new(base_url: "https://api.twitter.com/1.1/", **oauth_credentials)
41
+ v1_client = X::Client.new(base_url: "https://api.twitter.com/1.1/", **x_oauth_credentials)
42
42
 
43
43
  # Request your account settings
44
44
  v1_client.get("account/settings.json")
45
45
 
46
46
  # Initialize an X Ads API client
47
- ads_client = X::Client.new(base_url: "https://ads-api.twitter.com/12/", **oauth_credentials)
47
+ ads_client = X::Client.new(base_url: "https://ads-api.twitter.com/12/", **x_oauth_credentials)
48
48
 
49
49
  # Request your ad accounts
50
50
  ads_client.get("accounts")
51
51
  ```
52
52
 
53
+ ## History and Philosophy
54
+
55
+ This library is a rewrite of the [Twitter Ruby library](https://github.com/sferik/twitter). Over 16 years, that library ballooned to over 3,000 lines of code (plus 7,500 lines of tests). At the time of writing, this library is about 300 lines of code (plus 200 test lines) and I’d like to keep it that way. That doesn’t mean new features won’t be added over time, but the benefits of potential new features must be weighed against the benefits of simplicity:
56
+
57
+ * Less code is easier to maintain.
58
+ * Less code means fewer bugs.
59
+ * Less code runs faster.
60
+
61
+ In the immortal words of [Ezra Zygmuntowicz](https://github.com/ezmobius) and his [Merb](https://github.com/merb) project (may they both rest in peace): “No code is faster than no code.” The fastest code is the code that is never executed because it doesn’t exist. That principle should apply not just to this library itself but to third-party dependencies. At present, this library has one dependency ([oauth](https://rubygems.org/gems/oauth)) and I’d like to keep it that way. If anything, it should have fewer.
62
+
63
+ The tests for the previous version of this library ran in about 2 seconds. That sounds pretty fast until you see that tests for this library run in 2 hundredths of a second. This means you can automatically run the tests any time you write a file and receive immediate feedback. For such of workflows, 2 seconds feels painfully slow. At the same time, we aim to maintain 100% C0 code coverage.
64
+
65
+ This code is not littered with comments that are intended to generate documentation. Rather, this code is intended to be simple enough to serve as its own documentation. If you want to understand how something works, don’t read the documentation—it might be wrong—just read the code. The code is always right.
66
+
67
+ This project conforms to [Standard Ruby](https://github.com/standardrb/standard). Patches that don’t maintain that standard will not be accepted.
68
+
53
69
  ## Development
54
70
 
55
71
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/Rakefile CHANGED
@@ -7,8 +7,9 @@ Rake::TestTask.new(:test) do |t|
7
7
  t.test_files = FileList["test/**/test_*.rb"]
8
8
  end
9
9
 
10
+ require "standard/rake"
10
11
  require "rubocop/rake_task"
11
12
 
12
13
  RuboCop::RakeTask.new
13
14
 
14
- task default: %i[test rubocop]
15
+ task default: %i[test rubocop standard]
@@ -0,0 +1,43 @@
1
+ require "oauth"
2
+ require "forwardable"
3
+
4
+ module X
5
+ # Handles OAuth and bearer token authentication
6
+ class Authenticator
7
+ extend Forwardable
8
+
9
+ attr_accessor :bearer_token
10
+
11
+ def_delegator :@access_token, :secret, :access_token_secret
12
+ def_delegator :@access_token, :secret=, :access_token_secret=
13
+ def_delegator :@access_token, :token, :access_token
14
+ def_delegator :@access_token, :token=, :access_token=
15
+ def_delegator :@consumer, :key, :api_key
16
+ def_delegator :@consumer, :key=, :api_key=
17
+ def_delegator :@consumer, :secret, :api_key_secret
18
+ def_delegator :@consumer, :secret=, :api_key_secret=
19
+
20
+ def initialize(bearer_token:, api_key:, api_key_secret:, access_token:, access_token_secret:)
21
+ if bearer_token
22
+ @bearer_token = bearer_token
23
+ else
24
+ initialize_oauth(api_key, api_key_secret, access_token, access_token_secret)
25
+ end
26
+ end
27
+
28
+ def sign!(request)
29
+ @consumer.sign!(request, @access_token)
30
+ end
31
+
32
+ private
33
+
34
+ def initialize_oauth(api_key, api_key_secret, access_token, access_token_secret)
35
+ unless api_key && api_key_secret && access_token && access_token_secret
36
+ raise ArgumentError, "Missing OAuth credentials"
37
+ end
38
+
39
+ @consumer = OAuth::Consumer.new(api_key, api_key_secret, site: ClientDefaults::DEFAULT_BASE_URL)
40
+ @access_token = OAuth::Token.new(access_token, access_token_secret)
41
+ end
42
+ end
43
+ end
data/lib/x/client.rb CHANGED
@@ -1,100 +1,35 @@
1
1
  require "forwardable"
2
- require "json"
3
- require "net/http"
4
- require "oauth"
5
- require "uri"
6
- require_relative "version"
2
+ require_relative "authenticator"
3
+ require_relative "client_defaults"
4
+ require_relative "connection"
5
+ require_relative "request_builder"
6
+ require_relative "response_handler"
7
7
 
8
8
  module X
9
- JSON_CONTENT_TYPE = "application/json; charset=utf-8".freeze
10
-
11
- # Base error class
12
- class Error < ::StandardError
13
- attr_reader :object
14
-
15
- def initialize(response = nil)
16
- if response.is_a?(Net::HTTPResponse) && response.body && response["content-type"] == JSON_CONTENT_TYPE
17
- @object = JSON.parse(response.body)
18
- end
19
- super
20
- end
21
- end
22
-
23
- class NetworkError < Error; end
24
- class ClientError < Error; end
25
- class AuthenticationError < ClientError; end
26
- class BadRequestError < ClientError; end
27
- class ForbiddenError < ClientError; end
28
- class NotFoundError < ClientError; end
29
-
30
- # Rate limit error
31
- class TooManyRequestsError < ClientError
32
- def initialize(response = nil)
33
- @response = response
34
- super
35
- end
36
-
37
- def limit
38
- @response && @response["x-rate-limit-limit"]&.to_i
39
- end
40
-
41
- def remaining
42
- @response && @response["x-rate-limit-remaining"]&.to_i
43
- end
44
-
45
- def reset_at
46
- reset = @response && @response["x-rate-limit-reset"]&.to_i
47
- Time.at(reset.to_i).utc if reset
48
- end
49
-
50
- def reset_in
51
- [(reset_at - Time.now).ceil, 0].max if reset_at
52
- end
53
- alias retry_after reset_in
54
- end
55
-
56
- class ServerError < Error; end
57
- class ServiceUnavailableError < ServerError; end
58
-
59
- # HTTP client that handles authentication and requests
9
+ # Main public interface
60
10
  class Client
61
11
  extend Forwardable
12
+ include ClientDefaults
62
13
 
63
- attr_accessor :bearer_token, :user_agent, :read_timeout
64
- attr_reader :base_url
65
-
66
- def_delegator :@access_token, :secret, :access_token_secret
67
- def_delegator :@access_token, :secret=, :access_token_secret=
68
- def_delegator :@access_token, :token, :access_token
69
- def_delegator :@access_token, :token=, :access_token=
70
- def_delegator :@consumer, :key, :api_key
71
- def_delegator :@consumer, :key=, :api_key=
72
- def_delegator :@consumer, :secret, :api_key_secret
73
- def_delegator :@consumer, :secret=, :api_key_secret=
74
-
75
- DEFAULT_BASE_URL = "https://api.twitter.com/2/".freeze
76
- DEFAULT_USER_AGENT = "X-Client/#{X::Version} Ruby/#{RUBY_VERSION}".freeze
77
- DEFAULT_READ_TIMEOUT = 60 # seconds
78
- HTTP_METHODS = {
79
- get: Net::HTTP::Get,
80
- post: Net::HTTP::Post,
81
- put: Net::HTTP::Put,
82
- delete: Net::HTTP::Delete
83
- }.freeze
14
+ def_delegators :@authenticator, :bearer_token, :api_key, :api_key_secret, :access_token, :access_token_secret
15
+ def_delegators :@authenticator, :bearer_token=, :api_key=, :api_key_secret=, :access_token=, :access_token_secret=
16
+ def_delegators :@connection, :base_url, :open_timeout, :read_timeout, :write_timeout
17
+ def_delegators :@connection, :base_url=, :open_timeout=, :read_timeout=, :write_timeout=
18
+ def_delegators :@request_builder, :content_type, :user_agent
19
+ def_delegators :@request_builder, :content_type=, :user_agent=
20
+ def_delegators :@response_handler, :array_class, :object_class
21
+ def_delegators :@response_handler, :array_class=, :object_class=
84
22
 
85
23
  def initialize(bearer_token: nil, api_key: nil, api_key_secret: nil, access_token: nil, access_token_secret: nil,
86
- base_url: DEFAULT_BASE_URL, user_agent: DEFAULT_USER_AGENT, read_timeout: DEFAULT_READ_TIMEOUT)
87
- @base_url = URI(base_url)
88
- @user_agent = user_agent
89
- @read_timeout = read_timeout
90
-
91
- validate_base_url!
92
-
93
- if bearer_token.nil?
94
- initialize_oauth(api_key, api_key_secret, access_token, access_token_secret)
95
- else
96
- @bearer_token = bearer_token
97
- end
24
+ base_url: DEFAULT_BASE_URL, content_type: DEFAULT_CONTENT_TYPE, user_agent: DEFAULT_USER_AGENT,
25
+ open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT, write_timeout: DEFAULT_WRITE_TIMEOUT,
26
+ array_class: DEFAULT_ARRAY_CLASS, object_class: DEFAULT_OBJECT_CLASS)
27
+ @authenticator = Authenticator.new(bearer_token: bearer_token, api_key: api_key, api_key_secret: api_key_secret,
28
+ access_token: access_token, access_token_secret: access_token_secret)
29
+ @connection = Connection.new(base_url: base_url, open_timeout: open_timeout, read_timeout: read_timeout,
30
+ write_timeout: write_timeout)
31
+ @request_builder = RequestBuilder.new(content_type: content_type, user_agent: user_agent)
32
+ @response_handler = ResponseHandler.new(array_class: array_class, object_class: object_class)
98
33
  end
99
34
 
100
35
  def get(endpoint)
@@ -113,103 +48,13 @@ module X
113
48
  send_request(:delete, endpoint)
114
49
  end
115
50
 
116
- def base_url=(base_url)
117
- @base_url = URI(base_url)
118
- validate_base_url!
119
- end
120
-
121
51
  private
122
52
 
123
- def initialize_oauth(api_key, api_key_secret, access_token, access_token_secret)
124
- unless api_key && api_key_secret && access_token && access_token_secret
125
- raise ArgumentError, "Missing OAuth credentials"
126
- end
127
-
128
- @consumer = OAuth::Consumer.new(api_key, api_key_secret, site: @base_url)
129
- @access_token = OAuth::Token.new(access_token, access_token_secret)
130
- end
131
-
132
53
  def send_request(http_method, endpoint, body = nil)
133
- url = URI.join(@base_url, endpoint)
134
- http = Net::HTTP.new(url.host, url.port)
135
- http.use_ssl = url.scheme == "https"
136
- http.read_timeout = @read_timeout
137
-
138
- request = create_request(http_method, url, body)
139
- add_headers(request)
140
-
141
- handle_response(http.request(request))
142
- rescue Errno::ECONNREFUSED, Net::OpenTimeout, Net::ReadTimeout => e
143
- raise X::NetworkError, "Network error: #{e.message}"
144
- end
145
-
146
- def create_request(http_method, url, body)
147
- http_method_class = HTTP_METHODS[http_method]
148
-
149
- raise ArgumentError, "Unsupported HTTP method: #{http_method}" unless http_method_class
150
-
151
- request = http_method_class.new(url)
152
- request.body = body if body && http_method != :get
153
- request
154
- end
155
-
156
- def add_headers(request)
157
- add_authorization(request)
158
- add_content_type(request)
159
- add_user_agent(request)
160
- end
161
-
162
- def add_authorization(request)
163
- if @bearer_token.nil?
164
- @consumer.sign!(request, @access_token)
165
- else
166
- request["Authorization"] = "Bearer #{@bearer_token}"
167
- end
168
- end
169
-
170
- def add_content_type(request)
171
- request["Content-Type"] = JSON_CONTENT_TYPE
172
- end
173
-
174
- def add_user_agent(request)
175
- request["User-Agent"] = @user_agent if @user_agent
176
- end
177
-
178
- def validate_base_url!
179
- raise ArgumentError, "Invalid base URL" unless @base_url.is_a?(URI::HTTPS) || @base_url.is_a?(URI::HTTP)
180
- end
181
-
182
- def handle_response(response)
183
- ResponseHandler.new(response).handle
184
- end
185
-
186
- # HTTP client response handler
187
- class ResponseHandler
188
- ERROR_CLASSES = {
189
- Net::HTTPBadRequest => X::BadRequestError,
190
- Net::HTTPUnauthorized => X::AuthenticationError,
191
- Net::HTTPForbidden => X::ForbiddenError,
192
- Net::HTTPNotFound => X::NotFoundError,
193
- Net::HTTPTooManyRequests => X::TooManyRequestsError,
194
- Net::HTTPInternalServerError => X::ServerError,
195
- Net::HTTPServiceUnavailable => X::ServiceUnavailableError
196
- }.freeze
197
-
198
- def initialize(response)
199
- @response = response
200
- end
201
-
202
- def handle
203
- if @response.is_a?(Net::HTTPSuccess) && @response["content-type"] == JSON_CONTENT_TYPE
204
- return JSON.parse(@response.body)
205
- end
206
-
207
- error_class = ERROR_CLASSES[@response.class] || X::Error
208
- error_message = "#{@response.code} #{@response.message}"
209
- raise error_class, error_message if @response.body.nil? || @response.body.empty?
210
-
211
- raise error_class.new(@response), error_message
212
- end
54
+ request = @request_builder.build(authenticator: @authenticator, http_method: http_method, base_url: base_url,
55
+ endpoint: endpoint, body: body)
56
+ response = @connection.send_request(request: request)
57
+ @response_handler.handle(response: response)
213
58
  end
214
59
  end
215
60
  end
@@ -0,0 +1,14 @@
1
+ require_relative "version"
2
+
3
+ module X
4
+ module ClientDefaults
5
+ DEFAULT_BASE_URL = "https://api.twitter.com/2/".freeze
6
+ DEFAULT_CONTENT_TYPE = "application/json; charset=utf-8".freeze
7
+ DEFAULT_ARRAY_CLASS = Array
8
+ DEFAULT_OBJECT_CLASS = Hash
9
+ DEFAULT_OPEN_TIMEOUT = 60 # seconds
10
+ DEFAULT_READ_TIMEOUT = 60 # seconds
11
+ DEFAULT_WRITE_TIMEOUT = 60 # seconds
12
+ DEFAULT_USER_AGENT = "X-Client/#{VERSION} Ruby/#{RUBY_VERSION}".freeze
13
+ end
14
+ end
@@ -0,0 +1,40 @@
1
+ require "forwardable"
2
+ require "net/http"
3
+ require "uri"
4
+ require_relative "errors/errors"
5
+ require_relative "errors/network_error"
6
+
7
+ module X
8
+ # Sends HTTP requests
9
+ class Connection
10
+ extend Forwardable
11
+ include Errors
12
+
13
+ attr_reader :base_url
14
+
15
+ def_delegators :@http_client, :open_timeout, :read_timeout, :write_timeout
16
+ def_delegators :@http_client, :open_timeout=, :read_timeout=, :write_timeout=
17
+
18
+ def initialize(base_url:, open_timeout:, read_timeout:, write_timeout:)
19
+ self.base_url = URI(base_url)
20
+ @http_client = Net::HTTP.new(@base_url.host, @base_url.port)
21
+ @http_client.use_ssl = @base_url.scheme == "https"
22
+ @http_client.open_timeout = open_timeout
23
+ @http_client.read_timeout = read_timeout
24
+ @http_client.write_timeout = write_timeout
25
+ end
26
+
27
+ def send_request(request:)
28
+ @http_client.request(request)
29
+ rescue *NETWORK_ERRORS => e
30
+ raise NetworkError, "Network error: #{e.message}"
31
+ end
32
+
33
+ def base_url=(new_base_url)
34
+ uri = URI(new_base_url)
35
+ raise ArgumentError, "Invalid base URL" unless uri.is_a?(URI::HTTPS) || uri.is_a?(URI::HTTP)
36
+
37
+ @base_url = uri
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ require_relative "client_error"
2
+
3
+ module X
4
+ class AuthenticationError < ClientError; end
5
+ end
@@ -0,0 +1,5 @@
1
+ require_relative "client_error"
2
+
3
+ module X
4
+ class BadRequestError < ClientError; end
5
+ end
@@ -0,0 +1,5 @@
1
+ require_relative "error"
2
+
3
+ module X
4
+ class ClientError < Error; end
5
+ end
@@ -0,0 +1,24 @@
1
+ require "json"
2
+ require "net/http"
3
+ require_relative "../client_defaults"
4
+
5
+ module X
6
+ # Base error class
7
+ class Error < ::StandardError
8
+ include ClientDefaults
9
+ attr_reader :object
10
+
11
+ def initialize(msg = nil, response: nil, array_class: DEFAULT_ARRAY_CLASS, object_class: DEFAULT_OBJECT_CLASS)
12
+ if json_response?(response)
13
+ @object = JSON.parse(response.body, array_class: array_class, object_class: object_class)
14
+ end
15
+ super(msg)
16
+ end
17
+
18
+ private
19
+
20
+ def json_response?(response)
21
+ response.is_a?(Net::HTTPResponse) && response.body && response["content-type"] == DEFAULT_CONTENT_TYPE
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,28 @@
1
+ require "net/http"
2
+ require_relative "bad_request_error"
3
+ require_relative "authentication_error"
4
+ require_relative "forbidden_error"
5
+ require_relative "not_found_error"
6
+ require_relative "too_many_requests_error"
7
+ require_relative "server_error"
8
+ require_relative "service_unavailable_error"
9
+
10
+ module X
11
+ module Errors
12
+ ERROR_CLASSES = {
13
+ 400 => BadRequestError,
14
+ 401 => AuthenticationError,
15
+ 403 => ForbiddenError,
16
+ 404 => NotFoundError,
17
+ 429 => TooManyRequestsError,
18
+ 500 => ServerError,
19
+ 503 => ServiceUnavailableError
20
+ }.freeze
21
+
22
+ NETWORK_ERRORS = [
23
+ Errno::ECONNREFUSED,
24
+ Net::OpenTimeout,
25
+ Net::ReadTimeout
26
+ ].freeze
27
+ end
28
+ end
@@ -0,0 +1,5 @@
1
+ require_relative "client_error"
2
+
3
+ module X
4
+ class ForbiddenError < ClientError; end
5
+ end
@@ -0,0 +1,5 @@
1
+ require_relative "error"
2
+
3
+ module X
4
+ class NetworkError < Error; end
5
+ end
@@ -0,0 +1,5 @@
1
+ require_relative "client_error"
2
+
3
+ module X
4
+ class NotFoundError < ClientError; end
5
+ end
@@ -0,0 +1,5 @@
1
+ require_relative "error"
2
+
3
+ module X
4
+ class ServerError < Error; end
5
+ end
@@ -0,0 +1,5 @@
1
+ require_relative "server_error"
2
+
3
+ module X
4
+ class ServiceUnavailableError < ServerError; end
5
+ end
@@ -0,0 +1,32 @@
1
+ require_relative "client_error"
2
+ require_relative "../client_defaults"
3
+
4
+ module X
5
+ # Rate limit error
6
+ class TooManyRequestsError < ClientError
7
+ include ClientDefaults
8
+
9
+ def initialize(msg = nil, response: nil, array_class: DEFAULT_ARRAY_CLASS, object_class: DEFAULT_OBJECT_CLASS)
10
+ @response = response
11
+ super
12
+ end
13
+
14
+ def limit
15
+ @response&.fetch("x-rate-limit-limit", 0).to_i
16
+ end
17
+
18
+ def remaining
19
+ @response&.fetch("x-rate-limit-remaining", 0).to_i
20
+ end
21
+
22
+ def reset_at
23
+ Time.at(@response&.fetch("x-rate-limit-reset", 0).to_i).utc if @response
24
+ end
25
+
26
+ def reset_in
27
+ [(reset_at - Time.now).ceil, 0].max if reset_at
28
+ end
29
+
30
+ alias_method :retry_after, :reset_in
31
+ end
32
+ end
@@ -0,0 +1,58 @@
1
+ require "net/http"
2
+ require "uri"
3
+
4
+ module X
5
+ # Creates HTTP requests
6
+ class RequestBuilder
7
+ HTTP_METHODS = {
8
+ get: Net::HTTP::Get,
9
+ post: Net::HTTP::Post,
10
+ put: Net::HTTP::Put,
11
+ delete: Net::HTTP::Delete
12
+ }.freeze
13
+
14
+ attr_accessor :content_type, :user_agent
15
+
16
+ def initialize(content_type:, user_agent:)
17
+ @content_type = content_type
18
+ @user_agent = user_agent
19
+ end
20
+
21
+ def build(authenticator:, http_method:, base_url:, endpoint:, body: nil)
22
+ url = URI.join(base_url, endpoint)
23
+ request = create_request(http_method, url, body)
24
+ add_authorization(authenticator, request)
25
+ add_content_type(request)
26
+ add_user_agent(request)
27
+ request
28
+ end
29
+
30
+ private
31
+
32
+ def create_request(http_method, url, body)
33
+ http_method_class = HTTP_METHODS[http_method]
34
+
35
+ raise ArgumentError, "Unsupported HTTP method: #{http_method}" unless http_method_class
36
+
37
+ request = http_method_class.new(url)
38
+ request.body = body if body && http_method != :get
39
+ request
40
+ end
41
+
42
+ def add_authorization(authenticator, request)
43
+ if authenticator.bearer_token
44
+ request["Authorization"] = "Bearer #{@bearer_token}"
45
+ else
46
+ authenticator.sign!(request)
47
+ end
48
+ end
49
+
50
+ def add_content_type(request)
51
+ request["Content-Type"] = content_type if content_type
52
+ end
53
+
54
+ def add_user_agent(request)
55
+ request["User-Agent"] = user_agent if user_agent
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,40 @@
1
+ require "json"
2
+ require "net/http"
3
+ require_relative "errors/errors"
4
+
5
+ module X
6
+ # Process HTTP responses
7
+ class ResponseHandler
8
+ include ClientDefaults
9
+ include Errors
10
+
11
+ attr_accessor :array_class, :object_class
12
+
13
+ def initialize(array_class:, object_class:)
14
+ @array_class = array_class
15
+ @object_class = object_class
16
+ end
17
+
18
+ def handle(response:)
19
+ if successful_json_response?(response)
20
+ return JSON.parse(response.body, array_class: array_class, object_class: object_class)
21
+ end
22
+
23
+ error_class = ERROR_CLASSES[response.code.to_i] || Error
24
+ error_message = "#{response.code} #{response.message}"
25
+ raise error_class, error_message if empty_response_body?(response)
26
+
27
+ raise error_class.new(error_message, response: response, array_class: array_class, object_class: object_class)
28
+ end
29
+
30
+ private
31
+
32
+ def successful_json_response?(response)
33
+ response.is_a?(Net::HTTPSuccess) && response.body && response["content-type"] == DEFAULT_CONTENT_TYPE
34
+ end
35
+
36
+ def empty_response_body?(response)
37
+ response.body.nil? || response.body.empty?
38
+ end
39
+ end
40
+ end
data/lib/x/version.rb CHANGED
@@ -1,39 +1,5 @@
1
- module X
2
- # The version of this library
3
- module Version
4
- module_function
5
-
6
- def major
7
- 0
8
- end
9
-
10
- def minor
11
- 3
12
- end
13
-
14
- def patch
15
- 0
16
- end
1
+ require "rubygems/version"
17
2
 
18
- def pre
19
- nil
20
- end
21
-
22
- def to_h
23
- {
24
- major: major,
25
- minor: minor,
26
- patch: patch,
27
- pre: pre
28
- }
29
- end
30
-
31
- def to_a
32
- [major, minor, patch, pre].compact
33
- end
34
-
35
- def to_s
36
- to_a.join(".")
37
- end
38
- end
3
+ module X
4
+ VERSION = Gem::Version.create("0.5.0")
39
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: x
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erik Berlin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-08-04 00:00:00.000000000 Z
11
+ date: 2023-08-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oauth
@@ -38,7 +38,23 @@ files:
38
38
  - README.md
39
39
  - Rakefile
40
40
  - lib/x.rb
41
+ - lib/x/authenticator.rb
41
42
  - lib/x/client.rb
43
+ - lib/x/client_defaults.rb
44
+ - lib/x/connection.rb
45
+ - lib/x/errors/authentication_error.rb
46
+ - lib/x/errors/bad_request_error.rb
47
+ - lib/x/errors/client_error.rb
48
+ - lib/x/errors/error.rb
49
+ - lib/x/errors/errors.rb
50
+ - lib/x/errors/forbidden_error.rb
51
+ - lib/x/errors/network_error.rb
52
+ - lib/x/errors/not_found_error.rb
53
+ - lib/x/errors/server_error.rb
54
+ - lib/x/errors/service_unavailable_error.rb
55
+ - lib/x/errors/too_many_requests_error.rb
56
+ - lib/x/request_builder.rb
57
+ - lib/x/response_handler.rb
42
58
  - lib/x/version.rb
43
59
  - sig/x.rbs
44
60
  homepage: https://github.com/sferik/x-ruby
@@ -65,8 +81,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
65
81
  - !ruby/object:Gem::Version
66
82
  version: '0'
67
83
  requirements: []
68
- rubygems_version: 3.4.12
84
+ rubygems_version: 3.4.18
69
85
  signing_key:
70
86
  specification_version: 4
71
- summary: A Ruby interface to the X 2.0 API.
87
+ summary: A Ruby interface to the X API.
72
88
  test_files: []