x 0.2.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: c40d2b65cf1479d65e6f434d6d9c98cb32ad7aa62b739fd1207b491b7e276e4d
4
- data.tar.gz: 6dee4de59342deb6bbc8a5897f7a96e18ae6b7a856083ae84e1ee26580ce7482
3
+ metadata.gz: 2e5f85bad7fca01ea015e9a7acb13c6652ad6d06f651acacba1609f5299045ab
4
+ data.tar.gz: e10427f17c76a569c5bea2c6f50cc71d812ea9987db1c288db2e319d976760c8
5
5
  SHA512:
6
- metadata.gz: d64f01c2f011e53621214597fd1a9ac80e777ce22db2eb5d48c6b2f760743efa2fa3aa6848e6549b3fc25290c80456873262ddf92dbffaff2d134040cb87555f
7
- data.tar.gz: 8fc7d2aca33c67534ac775cc97f6afce95ef035716c5799e3571fb7b3a0218fd5b4646b41d8c66f61e3bba1d58f22b6c56c67ca273199b99dd7c55829827215a
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,18 @@
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
+
11
+ - Add accessors to X::Client (e61fa73)
12
+ - Add configurable read timeout (41502b9)
13
+ - Handle network-related errors (9ed1fb4)
14
+ - Include response body in errors (a203e6a)
15
+
3
16
  ## [0.2.0] - 2023-08-02
4
17
 
5
18
  - Allow configuration of base URL (4bc0531)
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,24 +15,57 @@ If bundler is not being used to manage dependencies, install the gem by executin
15
15
  ## Usage
16
16
 
17
17
  ```ruby
18
- x_api_key = "YOUR_X_API_KEY"
19
- x_api_key_secret = "YOUR_X_API_KEY_SECRET"
20
- x_access_token = "YOUR_X_ACCESS_TOKEN"
21
- x_access_token_secret = "YOUR_X_ACCESS_TOKEN_SECRET"
22
-
23
- x_client = X::Client.new(api_key: x_api_key,
24
- api_key_secret: x_api_key_secret,
25
- access_token: x_access_token,
26
- access_token_secret: x_access_token_secret)
27
-
28
- begin
29
- response = x_client.get("users/me")
30
- puts JSON.pretty_generate(response)
31
- rescue X::Error => e
32
- puts "Error: #{e.message}"
33
- end
18
+ x_oauth_credentials = {
19
+ api_key: "INSERT YOUR X API KEY HERE",
20
+ api_key_secret: "INSERT YOUR X API KEY SECRET HERE",
21
+ access_token: "INSERT YOUR X API ACCESS TOKEN HERE",
22
+ access_token_secret: "INSERT YOUR X API ACCESS TOKEN SECRET HERE",
23
+ }
24
+
25
+ # Initialize X API client with OAuth credentials
26
+ x_client = X::Client.new(**x_oauth_credentials)
27
+
28
+ # Request yourself
29
+ x_client.get("users/me")
30
+ # {"data"=>{"id"=>"7505382", "name"=>"Erik Berlin", "username"=>"sferik"}}
31
+
32
+ # Post a tweet
33
+ tweet = x_client.post("tweets", '{"text":"Hello, World! (from @gem)"}')
34
+ # {"data"=>{"edit_history_tweet_ids"=>["1234567890123456789"], "id"=>"1234567890123456789", "text"=>"Hello, World! (from @gem)"}}
35
+
36
+ # Delete a tweet
37
+ x_client.delete("tweets/#{tweet["data"]["id"]}")
38
+ # {"data"=>{"deleted"=>true}}
39
+
40
+ # Initialize an API v1.1 client
41
+ v1_client = X::Client.new(base_url: "https://api.twitter.com/1.1/", **x_oauth_credentials)
42
+
43
+ # Request your account settings
44
+ v1_client.get("account/settings.json")
45
+
46
+ # Initialize an X Ads API client
47
+ ads_client = X::Client.new(base_url: "https://ads-api.twitter.com/12/", **x_oauth_credentials)
48
+
49
+ # Request your ad accounts
50
+ ads_client.get("accounts")
34
51
  ```
35
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
+
36
69
  ## Development
37
70
 
38
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,194 +1,92 @@
1
- require "json"
2
- require "net/http"
3
- require "oauth"
1
+ require "forwardable"
2
+ require_relative "authenticator"
3
+ require_relative "client_defaults"
4
+ require_relative "connection"
5
+ require_relative "request_builder"
6
+ require_relative "response_handler"
4
7
 
5
8
  module X
6
- # HTTP client that handles authentication and requests
9
+ # Main public interface
7
10
  class Client
8
- DEFAULT_BASE_URL = "https://api.twitter.com/2/".freeze
9
- DEFAULT_USER_AGENT = "X-Client/#{VERSION} Ruby/#{RUBY_VERSION}".freeze
11
+ extend Forwardable
12
+ include ClientDefaults
13
+
14
+ attr_reader :base_url
15
+ attr_accessor :content_type, :open_timeout, :read_timeout, :user_agent, :array_class, :object_class
16
+
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=
10
19
 
11
20
  def initialize(bearer_token: nil, api_key: nil, api_key_secret: nil, access_token: nil, access_token_secret: nil,
12
- base_url: DEFAULT_BASE_URL, user_agent: DEFAULT_USER_AGENT)
13
- @http_request = HttpRequest.new(bearer_token: bearer_token,
14
- api_key: api_key,
15
- api_key_secret: api_key_secret,
16
- access_token: access_token,
17
- access_token_secret: access_token_secret,
18
- base_url: base_url,
19
- 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
30
+ @read_timeout = read_timeout
31
+ @user_agent = user_agent
32
+ @array_class = array_class
33
+ @object_class = object_class
20
34
  end
21
35
 
22
36
  def get(endpoint)
23
- handle_response { @http_request.get(endpoint) }
37
+ send_request(:get, endpoint)
24
38
  end
25
39
 
26
40
  def post(endpoint, body = nil)
27
- handle_response { @http_request.post(endpoint, body) }
41
+ send_request(:post, endpoint, body)
28
42
  end
29
43
 
30
44
  def put(endpoint, body = nil)
31
- handle_response { @http_request.put(endpoint, body) }
45
+ send_request(:put, endpoint, body)
32
46
  end
33
47
 
34
48
  def delete(endpoint)
35
- handle_response { @http_request.delete(endpoint) }
49
+ send_request(:delete, endpoint)
36
50
  end
37
51
 
38
- private
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)
39
55
 
40
- def handle_response
41
- response = yield
42
- ErrorHandler.new(response).handle
56
+ @base_url = uri
43
57
  end
44
58
 
45
- # HTTP client requester
46
- class HttpRequest
47
- HTTP_METHODS = {
48
- get: Net::HTTP::Get,
49
- post: Net::HTTP::Post,
50
- put: Net::HTTP::Put,
51
- delete: Net::HTTP::Delete
52
- }.freeze
53
-
54
- def initialize(bearer_token: nil, api_key: nil, api_key_secret: nil, access_token: nil, access_token_secret: nil,
55
- base_url: nil, user_agent: nil)
56
- @base_url = base_url
57
- @use_bearer_token = !bearer_token.nil?
58
- @user_agent = user_agent || Client::DEFAULT_USER_AGENT
59
-
60
- if @use_bearer_token
61
- @bearer_token = bearer_token
62
- else
63
- initialize_oauth(api_key, api_key_secret, access_token, access_token_secret)
64
- end
65
- end
66
-
67
- def get(endpoint)
68
- send_request(:get, endpoint)
69
- end
70
-
71
- def post(endpoint, body = nil)
72
- send_request(:post, endpoint, body)
73
- end
74
-
75
- def put(endpoint, body = nil)
76
- send_request(:put, endpoint, body)
77
- end
78
-
79
- def delete(endpoint)
80
- send_request(:delete, endpoint)
81
- end
82
-
83
- private
84
-
85
- def initialize_oauth(api_key, api_key_secret, access_token, access_token_secret)
86
- unless api_key && api_key_secret && access_token && access_token_secret
87
- raise ArgumentError, "Missing OAuth credentials."
88
- end
89
-
90
- @consumer = OAuth::Consumer.new(api_key, api_key_secret, site: @base_url)
91
- @access_token = OAuth::Token.new(access_token, access_token_secret)
92
- end
93
-
94
- def send_request(http_method, endpoint, body = nil)
95
- url = URI.parse(@base_url + endpoint)
96
- http = Net::HTTP.new(url.host, url.port)
97
- http.use_ssl = true
98
-
99
- request = create_request(http_method, url, body)
100
- add_authorization(request)
101
- add_user_agent(request)
102
-
103
- http.request(request)
104
- end
105
-
106
- def create_request(http_method, url, body)
107
- http_method_class = HTTP_METHODS[http_method]
59
+ private
108
60
 
109
- raise ArgumentError, "Unsupported HTTP method: #{http_method}" unless http_method_class
61
+ def send_request(http_method, endpoint, body = nil)
62
+ request = RequestBuilder.build(http_method, @base_url, endpoint, body)
63
+ add_headers(request)
110
64
 
111
- request = http_method_class.new(url)
112
- request.body = body if body && http_method != :get
113
- request
114
- end
65
+ response = Connection.send_request(@base_url, @open_timeout, @read_timeout, request)
115
66
 
116
- def add_authorization(request)
117
- if @use_bearer_token
118
- request["Authorization"] = "Bearer #{@bearer_token}"
119
- else
120
- @consumer.sign!(request, @access_token)
121
- end
122
- end
123
-
124
- def add_user_agent(request)
125
- request["User-Agent"] = @user_agent if @user_agent
126
- end
67
+ ResponseHandler.new(response, @array_class, @object_class).handle
127
68
  end
128
69
 
129
- # HTTP client error handler
130
- class ErrorHandler
131
- HTTP_STATUS_HANDLERS = {
132
- Net::HTTPOK => :handle_success_response,
133
- Net::HTTPBadRequest => :handle_bad_request_response,
134
- Net::HTTPForbidden => :handle_forbidden_response,
135
- Net::HTTPUnauthorized => :handle_unauthorized_response,
136
- Net::HTTPNotFound => :handle_not_found_response,
137
- Net::HTTPTooManyRequests => :handle_too_many_requests_response,
138
- Net::HTTPInternalServerError => :handle_server_error_response,
139
- Net::HTTPServiceUnavailable => :handle_service_unavailable_response
140
- }.freeze
141
-
142
- def initialize(response)
143
- @response = response
144
- end
145
-
146
- def handle
147
- handler_method = HTTP_STATUS_HANDLERS[@response.class]
148
- if handler_method
149
- send(handler_method)
150
- else
151
- handle_unexpected_response
152
- end
153
- end
154
-
155
- private
156
-
157
- def handle_success_response
158
- JSON.parse(@response.body)
159
- end
160
-
161
- def handle_bad_request_response
162
- raise X::BadRequestError, "Bad request: #{@response.code} #{@response.message}"
163
- end
164
-
165
- def handle_forbidden_response
166
- raise X::ForbiddenError, "Forbidden: #{@response.code} #{@response.message}"
167
- end
168
-
169
- def handle_unauthorized_response
170
- raise X::AuthenticationError, "Authentication failed. Please check your credentials."
171
- end
172
-
173
- def handle_not_found_response
174
- raise X::NotFoundError, "Not found: #{@response.code} #{@response.message}"
175
- end
176
-
177
- def handle_too_many_requests_response
178
- raise X::TooManyRequestsError, "Too many requests: #{@response.code} #{@response.message}"
179
- end
70
+ def add_headers(request)
71
+ add_authorization(request)
72
+ add_content_type(request)
73
+ add_user_agent(request)
74
+ end
180
75
 
181
- def handle_server_error_response
182
- raise X::ServerError, "Internal server error: #{@response.code} #{@response.message}"
76
+ def add_authorization(request)
77
+ if @authenticator.bearer_token
78
+ request["Authorization"] = "Bearer #{@bearer_token}"
79
+ else
80
+ @authenticator.sign!(request)
183
81
  end
82
+ end
184
83
 
185
- def handle_service_unavailable_response
186
- raise X::ServiceUnavailableError, "Service unavailable: #{@response.code} #{@response.message}"
187
- end
84
+ def add_content_type(request)
85
+ request["Content-Type"] = @content_type if @content_type
86
+ end
188
87
 
189
- def handle_unexpected_response
190
- raise X::Error, "Unexpected response: #{@response.code}"
191
- end
88
+ def add_user_agent(request)
89
+ request["User-Agent"] = @user_agent if @user_agent
192
90
  end
193
91
  end
194
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
@@ -1,3 +1,39 @@
1
1
  module X
2
- VERSION = "0.2.0".freeze
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
+ 4
12
+ end
13
+
14
+ def patch
15
+ 0
16
+ end
17
+
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
39
  end
data/lib/x.rb CHANGED
@@ -1,3 +1 @@
1
- require_relative "x/version"
2
- require_relative "x/errors"
3
1
  require_relative "x/client"
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.2.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-02 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,8 +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
42
- - lib/x/errors.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
43
58
  - lib/x/version.rb
44
59
  - sig/x.rbs
45
60
  homepage: https://github.com/sferik/x-ruby
@@ -66,8 +81,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
66
81
  - !ruby/object:Gem::Version
67
82
  version: '0'
68
83
  requirements: []
69
- rubygems_version: 3.4.12
84
+ rubygems_version: 3.4.18
70
85
  signing_key:
71
86
  specification_version: 4
72
- summary: A Ruby interface to the X 2.0 API.
87
+ summary: A Ruby interface to the X API.
73
88
  test_files: []
data/lib/x/errors.rb DELETED
@@ -1,11 +0,0 @@
1
- module X
2
- class Error < ::StandardError; end
3
- class ClientError < Error; end
4
- class AuthenticationError < ClientError; end
5
- class BadRequestError < ClientError; end
6
- class ForbiddenError < ClientError; end
7
- class NotFoundError < ClientError; end
8
- class TooManyRequestsError < ClientError; end
9
- class ServerError < Error; end
10
- class ServiceUnavailableError < ServerError; end
11
- end