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 +4 -4
- data/.rubocop.yml +35 -3
- data/CHANGELOG.md +8 -0
- data/Gemfile +2 -0
- data/README.md +21 -5
- data/Rakefile +2 -1
- data/lib/x/authenticator.rb +43 -0
- data/lib/x/client.rb +34 -157
- data/lib/x/client_defaults.rb +13 -0
- data/lib/x/connection.rb +21 -0
- data/lib/x/errors/authentication_error.rb +5 -0
- data/lib/x/errors/bad_request_error.rb +5 -0
- data/lib/x/errors/client_error.rb +5 -0
- data/lib/x/errors/error.rb +20 -0
- data/lib/x/errors/errors.rb +27 -0
- data/lib/x/errors/forbidden_error.rb +5 -0
- data/lib/x/errors/network_error.rb +5 -0
- data/lib/x/errors/not_found_error.rb +5 -0
- data/lib/x/errors/server_error.rb +5 -0
- data/lib/x/errors/service_unavailable_error.rb +5 -0
- data/lib/x/errors/too_many_requests_error.rb +30 -0
- data/lib/x/request_builder.rb +29 -0
- data/lib/x/response_handler.rb +34 -0
- data/lib/x/version.rb +1 -1
- metadata +20 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2e5f85bad7fca01ea015e9a7acb13c6652ad6d06f651acacba1609f5299045ab
|
4
|
+
data.tar.gz: e10427f17c76a569c5bea2c6f50cc71d812ea9987db1c288db2e319d976760c8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
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(**
|
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/", **
|
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/", **
|
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
@@ -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
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
require_relative "
|
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
|
-
|
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
|
-
|
67
|
-
|
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
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
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=(
|
117
|
-
|
118
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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"] =
|
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
|
data/lib/x/connection.rb
ADDED
@@ -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,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,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
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.
|
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-
|
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.
|
84
|
+
rubygems_version: 3.4.18
|
69
85
|
signing_key:
|
70
86
|
specification_version: 4
|
71
|
-
summary: A Ruby interface to the X
|
87
|
+
summary: A Ruby interface to the X API.
|
72
88
|
test_files: []
|