x 0.3.0 → 0.4.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: 2e5f85bad7fca01ea015e9a7acb13c6652ad6d06f651acacba1609f5299045ab
4
+ data.tar.gz: e10427f17c76a569c5bea2c6f50cc71d812ea9987db1c288db2e319d976760c8
5
5
  SHA512:
6
- metadata.gz: c5b386cd7386457b148c555359e68c4f9cb72b598ff9eb0a75405f3af75599dca0d5fc1700b01653465b2e50e401b69e9f9e5e9dd126d6a56c46b6df96b5736c
7
- data.tar.gz: 462957aeee740d46f4a83497e69bec852fa0a4d8a9e38aec6bf8ffcd8f432e862737fa34847995dce5a49c09a40d9a08607ace68a1314a32972292d881cbd1db
6
+ metadata.gz: f5ab83ea7ee748d3e17c43bc5f7ce55a59685e7d4e10039c5e7cc28f0bca7b31c689e868f763c5f84b7ffcc9ea92f850b578cc14fede2c29cd24e73cad4c4548
7
+ data.tar.gz: 01cf440dbad6c968d4cf8b83b4ac496d03309904c9d2c2b1aa01a0bd8d2894a5bfec1990ec7a01de488098b69917e82216cd1d53828c3d594ab49a7a6436e6b2
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,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2023-08-06
4
+
5
+ - Refactor Client into Authenticator, RequestBuilder, Connection, ResponseHandler (6bee1e9)
6
+ - Add configurable open timeout (1000f9d)
7
+ - Allow configuration of content type (f33a732)
8
+
9
+ ## [0.3.0] - 2023-08-04
10
+
3
11
  - Add accessors to X::Client (e61fa73)
4
12
  - Add configurable read timeout (41502b9)
5
13
  - 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 200 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,36 @@
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
14
  attr_reader :base_url
15
+ attr_accessor :content_type, :open_timeout, :read_timeout, :user_agent, :array_class, :object_class
65
16
 
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
17
+ def_delegators :@authenticator, :bearer_token, :api_key, :api_key_secret, :access_token, :access_token_secret
18
+ def_delegators :@authenticator, :bearer_token=, :api_key=, :api_key_secret=, :access_token=, :access_token_secret=
84
19
 
85
20
  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
21
+ base_url: DEFAULT_BASE_URL, content_type: DEFAULT_CONTENT_TYPE,
22
+ open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT, user_agent: DEFAULT_USER_AGENT,
23
+ array_class: DEFAULT_ARRAY_CLASS, object_class: DEFAULT_OBJECT_CLASS)
24
+
25
+ @authenticator = Authenticator.new(bearer_token: bearer_token, api_key: api_key, api_key_secret: api_key_secret,
26
+ access_token: access_token, access_token_secret: access_token_secret)
27
+ self.base_url = base_url
28
+ @content_type = content_type
29
+ @open_timeout = open_timeout
89
30
  @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
31
+ @user_agent = user_agent
32
+ @array_class = array_class
33
+ @object_class = object_class
98
34
  end
99
35
 
100
36
  def get(endpoint)
@@ -113,44 +49,22 @@ module X
113
49
  send_request(:delete, endpoint)
114
50
  end
115
51
 
116
- def base_url=(base_url)
117
- @base_url = URI(base_url)
118
- validate_base_url!
52
+ def base_url=(new_base_url)
53
+ uri = URI(new_base_url)
54
+ raise ArgumentError, "Invalid base URL" unless uri.is_a?(URI::HTTPS) || uri.is_a?(URI::HTTP)
55
+
56
+ @base_url = uri
119
57
  end
120
58
 
121
59
  private
122
60
 
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
61
  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)
62
+ request = RequestBuilder.build(http_method, @base_url, endpoint, body)
139
63
  add_headers(request)
140
64
 
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]
65
+ response = Connection.send_request(@base_url, @open_timeout, @read_timeout, request)
148
66
 
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
67
+ ResponseHandler.new(response, @array_class, @object_class).handle
154
68
  end
155
69
 
156
70
  def add_headers(request)
@@ -160,56 +74,19 @@ module X
160
74
  end
161
75
 
162
76
  def add_authorization(request)
163
- if @bearer_token.nil?
164
- @consumer.sign!(request, @access_token)
165
- else
77
+ if @authenticator.bearer_token
166
78
  request["Authorization"] = "Bearer #{@bearer_token}"
79
+ else
80
+ @authenticator.sign!(request)
167
81
  end
168
82
  end
169
83
 
170
84
  def add_content_type(request)
171
- request["Content-Type"] = JSON_CONTENT_TYPE
85
+ request["Content-Type"] = @content_type if @content_type
172
86
  end
173
87
 
174
88
  def add_user_agent(request)
175
89
  request["User-Agent"] = @user_agent if @user_agent
176
90
  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
213
- end
214
91
  end
215
92
  end
@@ -0,0 +1,13 @@
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_USER_AGENT = "X-Client/#{Version} Ruby/#{RUBY_VERSION}".freeze
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ require "net/http"
2
+ require_relative "errors/network_error"
3
+ require_relative "errors/errors"
4
+
5
+ module X
6
+ # Sends HTTP requests
7
+ class Connection
8
+ include Errors
9
+
10
+ def self.send_request(base_url, open_timeout, read_timeout, request)
11
+ url = URI(base_url)
12
+ http = Net::HTTP.new(url.host, url.port)
13
+ http.use_ssl = url.scheme == "https"
14
+ http.open_timeout = open_timeout
15
+ http.read_timeout = read_timeout
16
+ http.request(request)
17
+ rescue *NETWORK_ERRORS => e
18
+ raise NetworkError, "Network error: #{e.message}"
19
+ end
20
+ end
21
+ 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,20 @@
1
+ require "json"
2
+
3
+ module X
4
+ # Base error class
5
+ class Error < ::StandardError
6
+ include ClientDefaults
7
+ attr_reader :object
8
+
9
+ def initialize(msg = nil, response = nil, object_class = DEFAULT_OBJECT_CLASS)
10
+ @object = JSON.parse(response.body, object_class: object_class) if json_response?(response)
11
+ super(msg)
12
+ end
13
+
14
+ private
15
+
16
+ def json_response?(response)
17
+ response.is_a?(Net::HTTPResponse) && response.body && response["content-type"] == DEFAULT_CONTENT_TYPE
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ require_relative "bad_request_error"
2
+ require_relative "authentication_error"
3
+ require_relative "forbidden_error"
4
+ require_relative "not_found_error"
5
+ require_relative "too_many_requests_error"
6
+ require_relative "server_error"
7
+ require_relative "service_unavailable_error"
8
+
9
+ module X
10
+ module Errors
11
+ ERROR_CLASSES = {
12
+ 400 => BadRequestError,
13
+ 401 => AuthenticationError,
14
+ 403 => ForbiddenError,
15
+ 404 => NotFoundError,
16
+ 429 => TooManyRequestsError,
17
+ 500 => ServerError,
18
+ 503 => ServiceUnavailableError
19
+ }.freeze
20
+
21
+ NETWORK_ERRORS = [
22
+ Errno::ECONNREFUSED,
23
+ Net::OpenTimeout,
24
+ Net::ReadTimeout
25
+ ].freeze
26
+ end
27
+ 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,30 @@
1
+ require "net/http"
2
+ require_relative "client_error"
3
+
4
+ module X
5
+ # Rate limit error
6
+ class TooManyRequestsError < ClientError
7
+ def initialize(msg, response = nil, object_class = ClientDefaults::DEFAULT_OBJECT_CLASS)
8
+ @response = response
9
+ super
10
+ end
11
+
12
+ def limit
13
+ @response&.fetch("x-rate-limit-limit", 0).to_i
14
+ end
15
+
16
+ def remaining
17
+ @response&.fetch("x-rate-limit-remaining", 0).to_i
18
+ end
19
+
20
+ def reset_at
21
+ Time.at(@response&.fetch("x-rate-limit-reset", 0).to_i).utc if @response
22
+ end
23
+
24
+ def reset_in
25
+ [(reset_at - Time.now).ceil, 0].max if reset_at
26
+ end
27
+
28
+ alias_method :retry_after, :reset_in
29
+ end
30
+ end
@@ -0,0 +1,29 @@
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
+ def self.build(http_method, base_url, endpoint, body = nil)
15
+ url = URI.join(base_url, endpoint)
16
+ create_request(http_method, url, body)
17
+ end
18
+
19
+ def self.create_request(http_method, url, body)
20
+ http_method_class = HTTP_METHODS[http_method]
21
+
22
+ raise ArgumentError, "Unsupported HTTP method: #{http_method}" unless http_method_class
23
+
24
+ request = http_method_class.new(url)
25
+ request.body = body if body && http_method != :get
26
+ request
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,34 @@
1
+ require "json"
2
+ require_relative "errors/errors"
3
+
4
+ module X
5
+ # Process HTTP responses
6
+ class ResponseHandler
7
+ include ClientDefaults
8
+ include Errors
9
+
10
+ def initialize(response, array_class, object_class)
11
+ @response = response
12
+ @array_class = array_class
13
+ @object_class = object_class
14
+ end
15
+
16
+ def handle
17
+ if successful_json_response?
18
+ return JSON.parse(@response.body, array_class: @array_class, object_class: @object_class)
19
+ end
20
+
21
+ error_class = ERROR_CLASSES[@response.code.to_i] || Error
22
+ error_message = "#{@response.code} #{@response.message}"
23
+ raise error_class, error_message if @response.body.nil? || @response.body.empty?
24
+
25
+ raise error_class.new(error_message, @response)
26
+ end
27
+
28
+ private
29
+
30
+ def successful_json_response?
31
+ @response.is_a?(Net::HTTPSuccess) && @response.body && @response["content-type"] == DEFAULT_CONTENT_TYPE
32
+ end
33
+ end
34
+ end
data/lib/x/version.rb CHANGED
@@ -8,7 +8,7 @@ module X
8
8
  end
9
9
 
10
10
  def minor
11
- 3
11
+ 4
12
12
  end
13
13
 
14
14
  def patch
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.4.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-06 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: []