x 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -28
  3. data/README.md +12 -2
  4. data/lib/x/authenticator.rb +10 -0
  5. data/lib/x/bearer_token_authenticator.rb +5 -3
  6. data/lib/x/cgi.rb +15 -0
  7. data/lib/x/client.rb +85 -49
  8. data/lib/x/connection.rb +39 -76
  9. data/lib/x/errors/bad_gateway.rb +5 -0
  10. data/lib/x/errors/{not_found_error.rb → bad_request.rb} +1 -1
  11. data/lib/x/errors/client_error.rb +2 -2
  12. data/lib/x/errors/connection_exception.rb +5 -0
  13. data/lib/x/errors/error.rb +1 -11
  14. data/lib/x/errors/{forbidden_error.rb → forbidden.rb} +1 -1
  15. data/lib/x/errors/gateway_timeout.rb +5 -0
  16. data/lib/x/errors/{bad_request_error.rb → gone.rb} +1 -1
  17. data/lib/x/errors/http_error.rb +42 -0
  18. data/lib/x/errors/network_error.rb +1 -1
  19. data/lib/x/errors/not_acceptable.rb +5 -0
  20. data/lib/x/errors/not_found.rb +5 -0
  21. data/lib/x/errors/payload_too_large.rb +5 -0
  22. data/lib/x/errors/server_error.rb +2 -2
  23. data/lib/x/errors/service_unavailable.rb +5 -0
  24. data/lib/x/errors/too_many_redirects.rb +5 -0
  25. data/lib/x/errors/too_many_requests.rb +24 -0
  26. data/lib/x/errors/unauthorized.rb +5 -0
  27. data/lib/x/errors/unprocessable_entity.rb +5 -0
  28. data/lib/x/{media_upload.rb → media_uploader.rb} +34 -35
  29. data/lib/x/oauth_authenticator.rb +10 -15
  30. data/lib/x/redirect_handler.rb +26 -23
  31. data/lib/x/request_builder.rb +22 -35
  32. data/lib/x/response_parser.rb +69 -0
  33. data/lib/x/version.rb +1 -1
  34. data/sig/x.rbs +118 -92
  35. metadata +22 -13
  36. data/lib/x/errors/authentication_error.rb +0 -5
  37. data/lib/x/errors/payload_too_large_error.rb +0 -5
  38. data/lib/x/errors/service_unavailable_error.rb +0 -5
  39. data/lib/x/errors/too_many_redirects_error.rb +0 -5
  40. data/lib/x/errors/too_many_requests_error.rb +0 -29
  41. data/lib/x/response_handler.rb +0 -63
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a01e8a21951f3b998cfae0253c4d35c5412ac39f94681283b9059d4a196651e
4
- data.tar.gz: 5b4a5bb02d86b27391b005acb329893025222c20e9d0c99b88ed0d7e6322dc5e
3
+ metadata.gz: 5c6252a6765246fe6ca0820e5ce96f9e66d9611e91b2d59753730fad6b87f67e
4
+ data.tar.gz: 7a7dc55ece60d848cde08c151e887a1cc86808408a864c05934c4be906b2021a
5
5
  SHA512:
6
- metadata.gz: 8972eafdb4c041a61258f409fef5a93dc0ff56b70e5c8dd384e126652c1ab32a4e52dc56bd253d7c204ea6313e3ac321e93c7245c722c0d5491b59443c63435a
7
- data.tar.gz: 4a5dfb958b61b78cdcf9312250d3ba00182894241da1c3e9114f08f1029fb174be07043f607a864986e3d09d6d308dfac4d13549e2483d5746235b34c4dd05a2
6
+ metadata.gz: 4216ad5a466dd1a714a638a45d9c0996f40c16247f7fc6eb0ff756d301c3cd63aac5ca95fd421940634e3661db5cb41e19300b73f8ef01056c14e21ad89edea1
7
+ data.tar.gz: a27036e7dd594f9b13b5cd66267c57b63679352ea3eeeaada13adbba838c6cdad5d8f841fe35b808950fc610808c219679e72334091b3d8de1dca5899ba386b5
data/CHANGELOG.md CHANGED
@@ -1,69 +1,88 @@
1
+ ## [0.12.0] - 2023-11-02
2
+ * Ensure Authenticator is passed to RedirectHandler (fc8557b)
3
+ * Add AUTHENTICATION_HEADER to X::Authenticator base class (85a2818)
4
+ * Introduce X::HTTPError (90ae132)
5
+ * Add `code` attribute to error classes (b003639)
6
+
7
+ ## [0.11.0] - 2023-10-24
8
+
9
+ * Add base Authenticator class (8c66ce2)
10
+ * Consistently use keyword arguments (3beb271)
11
+ * Use patern matching to build request (4d001c7)
12
+ * Rename ResponseHandler to ResponseParser (498e890)
13
+ * Rename methods to be more consistent (5b8c655)
14
+ * Rename MediaUpload to MediaUploader (84f0c15)
15
+ * Add mutant and kill mutants (b124968)
16
+ * Fix authentication bug with request URLs that contain spaces (8de3174)
17
+ * Refactor errors (853d39c)
18
+ * Make Connection class threadsafe (d95d285)
19
+
1
20
  ## [0.10.0] - 2023-10-08
2
21
 
3
- - Add media upload helper methods (6c6a267)
4
- - Add PayloadTooLargeError class (cd61850)
22
+ * Add media upload helper methods (6c6a267)
23
+ * Add PayloadTooLargeError class (cd61850)
5
24
 
6
25
  ## [0.9.1] - 2023-10-06
7
26
 
8
- - Allow successful empty responses (06bf7db)
9
- - Update default User-Agent string (296b36a)
10
- - Move query parameter escaping into RequestBuilder (56d6bd2)
27
+ * Allow successful empty responses (06bf7db)
28
+ * Update default User-Agent string (296b36a)
29
+ * Move query parameter escaping into RequestBuilder (56d6bd2)
11
30
 
12
31
  ## [0.9.0] - 2023-09-26
13
32
 
14
- - Add support for HTTP proxies (3740f4f)
33
+ * Add support for HTTP proxies (3740f4f)
15
34
 
16
35
  ## [0.8.1] - 2023-09-20
17
36
 
18
- - Fix bug where setting Connection#base_uri= doesn't update the HTTP client (d5a89db)
37
+ * Fix bug where setting Connection#base_uri= doesn't update the HTTP client (d5a89db)
19
38
 
20
39
  ## [0.8.0] - 2023-09-14
21
40
 
22
- - Add (back) bearer token authentication (62e141d)
23
- - Follow redirects (90a8c55)
24
- - Parse error responses with Content-Type: application/problem+json (0b697d9)
41
+ * Add (back) bearer token authentication (62e141d)
42
+ * Follow redirects (90a8c55)
43
+ * Parse error responses with Content-Type: application/problem+json (0b697d9)
25
44
 
26
45
  ## [0.7.1] - 2023-09-02
27
46
 
28
- - Fix bug in X::Authenticator#split_uri (ebc9d5f)
47
+ * Fix bug in X::Authenticator#split_uri (ebc9d5f)
29
48
 
30
49
  ## [0.7.0] - 2023-09-02
31
50
 
32
- - Remove OAuth gem (7c29bb1)
51
+ * Remove OAuth gem (7c29bb1)
33
52
 
34
53
  ## [0.6.0] - 2023-08-30
35
54
 
36
- - Add configurable debug output stream for logging (fd2d4b0)
37
- - Remove bearer token authentication (efff940)
38
- - Define RBS type signatures (d7f63ba)
55
+ * Add configurable debug output stream for logging (fd2d4b0)
56
+ * Remove bearer token authentication (efff940)
57
+ * Define RBS type signatures (d7f63ba)
39
58
 
40
59
  ## [0.5.1] - 2023-08-16
41
60
 
42
- - Fix bearer token authentication (1a1ca93)
61
+ * Fix bearer token authentication (1a1ca93)
43
62
 
44
63
  ## [0.5.0] - 2023-08-10
45
64
 
46
- - Add configurable write timeout (2a31f84)
47
- - Use built-in Gem::Version class (066e0b6)
65
+ * Add configurable write timeout (2a31f84)
66
+ * Use built-in Gem::Version class (066e0b6)
48
67
 
49
68
  ## [0.4.0] - 2023-08-06
50
69
 
51
- - Refactor Client into Authenticator, RequestBuilder, Connection, ResponseHandler (6bee1e9)
52
- - Add configurable open timeout (1000f9d)
53
- - Allow configuration of content type (f33a732)
70
+ * Refactor Client into Authenticator, RequestBuilder, Connection, ResponseHandler (6bee1e9)
71
+ * Add configurable open timeout (1000f9d)
72
+ * Allow configuration of content type (f33a732)
54
73
 
55
74
  ## [0.3.0] - 2023-08-04
56
75
 
57
- - Add accessors to X::Client (e61fa73)
58
- - Add configurable read timeout (41502b9)
59
- - Handle network-related errors (9ed1fb4)
60
- - Include response body in errors (a203e6a)
76
+ * Add accessors to X::Client (e61fa73)
77
+ * Add configurable read timeout (41502b9)
78
+ * Handle network-related errors (9ed1fb4)
79
+ * Include response body in errors (a203e6a)
61
80
 
62
81
  ## [0.2.0] - 2023-08-02
63
82
 
64
- - Allow configuration of base URL (4bc0531)
65
- - Improve error handling (14dc0cd)
83
+ * Allow configuration of base URL (4bc0531)
84
+ * Improve error handling (14dc0cd)
66
85
 
67
86
  ## [0.1.0] - 2023-08-02
68
87
 
69
- - Initial release
88
+ * Initial release
data/README.md CHANGED
@@ -1,3 +1,9 @@
1
+ ![Tests](https://github.com/sferik/x-ruby/actions/workflows/test.yml/badge.svg)
2
+ ![Linter](https://github.com/sferik/x-ruby/actions/workflows/lint.yml/badge.svg)
3
+ ![Mutant](https://github.com/sferik/x-ruby/actions/workflows/mutant.yml/badge.svg)
4
+ ![Typer Checker](https://github.com/sferik/x-ruby/actions/workflows/type_check.yml/badge.svg)
5
+ [![Gem Version](https://badge.fury.io/rb/x.svg)](https://rubygems.org/gems/x)
6
+
1
7
  # A [Ruby](https://www.ruby-lang.org) interface to the [X API](https://developer.x.com)
2
8
 
3
9
  ## Follow
@@ -130,11 +136,15 @@ Pull requests will only be accepted if they meet all the following criteria:
130
136
 
131
137
  bundle exec rake standard
132
138
 
133
- 2. For any new code paths, tests must be added to maintain 100% C0 code coverage. This can be verified with:
139
+ 2. 100% C0 code coverage. This can be verified with:
134
140
 
135
141
  bundle exec rake test
136
142
 
137
- 3. For any new classes or methods, RBS type signatures must be added (to sig/x.rbs). This can be verified with:
143
+ 3. 100% mutation coverage. This can be verified with:
144
+
145
+ bundle exec rake mutant
146
+
147
+ 4. RBS type signatures (in `sig/x.rbs`). This can be verified with:
138
148
 
139
149
  bundle exec rake steep
140
150
 
@@ -0,0 +1,10 @@
1
+ module X
2
+ # Base Authenticator class
3
+ class Authenticator
4
+ AUTHENTICATION_HEADER = "Authorization".freeze
5
+
6
+ def header(_request)
7
+ {AUTHENTICATION_HEADER => ""}
8
+ end
9
+ end
10
+ end
@@ -1,14 +1,16 @@
1
+ require_relative "authenticator"
2
+
1
3
  module X
2
4
  # Handles bearer token authentication
3
- class BearerTokenAuthenticator
5
+ class BearerTokenAuthenticator < Authenticator
4
6
  attr_accessor :bearer_token
5
7
 
6
- def initialize(bearer_token)
8
+ def initialize(bearer_token:) # rubocop:disable Lint/MissingSuper
7
9
  @bearer_token = bearer_token
8
10
  end
9
11
 
10
12
  def header(_request)
11
- "Bearer #{bearer_token}"
13
+ {AUTHENTICATION_HEADER => "Bearer #{bearer_token}"}
12
14
  end
13
15
  end
14
16
  end
data/lib/x/cgi.rb ADDED
@@ -0,0 +1,15 @@
1
+ require "cgi"
2
+
3
+ module X
4
+ # Namespaced CGI class
5
+ class CGI
6
+ # TODO: Replace CGI.escape with CGI.escapeURIComponent when support for Ruby 3.1 is dropped
7
+ def self.escape(value)
8
+ ::CGI.escape(value).gsub("+", "%20")
9
+ end
10
+
11
+ def self.escape_params(params)
12
+ params.map { |k, v| "#{k}=#{escape(v)}" }.join("&")
13
+ end
14
+ end
15
+ end
data/lib/x/client.rb CHANGED
@@ -1,87 +1,123 @@
1
1
  require "forwardable"
2
2
  require_relative "bearer_token_authenticator"
3
- require_relative "oauth_authenticator"
4
3
  require_relative "connection"
4
+ require_relative "oauth_authenticator"
5
5
  require_relative "redirect_handler"
6
6
  require_relative "request_builder"
7
- require_relative "response_handler"
7
+ require_relative "response_parser"
8
8
 
9
9
  module X
10
10
  # Main public interface
11
11
  class Client
12
12
  extend Forwardable
13
13
 
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_uri, :open_timeout, :read_timeout, :write_timeout, :debug_output
17
- def_delegators :@connection, :base_uri=, :open_timeout=, :read_timeout=, :write_timeout=, :debug_output=
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=
22
- alias_method :base_url, :base_uri
23
- alias_method :base_url=, :base_uri=
24
- attr_accessor :authenticator, :connection, :request_builder, :redirect_handler, :response_handler
25
-
26
- def initialize(bearer_token: nil,
27
- api_key: nil, api_key_secret: nil, access_token: nil, access_token_secret: nil,
28
- base_url: Connection::DEFAULT_BASE_URL,
14
+ DEFAULT_BASE_URL = "https://api.twitter.com/2/".freeze
15
+
16
+ attr_accessor :base_url
17
+ attr_reader :api_key, :api_key_secret, :access_token, :access_token_secret, :bearer_token
18
+
19
+ def_delegators :@connection, :open_timeout, :read_timeout, :write_timeout, :proxy_url, :debug_output
20
+ def_delegators :@connection, :open_timeout=, :read_timeout=, :write_timeout=, :proxy_url=, :debug_output=
21
+ def_delegators :@redirect_handler, :max_redirects
22
+ def_delegators :@redirect_handler, :max_redirects=
23
+ def_delegators :@response_parser, :array_class, :object_class
24
+ def_delegators :@response_parser, :array_class=, :object_class=
25
+
26
+ def initialize(api_key: nil, api_key_secret: nil, access_token: nil, access_token_secret: nil,
27
+ bearer_token: nil,
28
+ base_url: DEFAULT_BASE_URL,
29
29
  open_timeout: Connection::DEFAULT_OPEN_TIMEOUT,
30
30
  read_timeout: Connection::DEFAULT_READ_TIMEOUT,
31
31
  write_timeout: Connection::DEFAULT_WRITE_TIMEOUT,
32
+ debug_output: Connection::DEFAULT_DEBUG_OUTPUT,
32
33
  proxy_url: nil,
33
- content_type: RequestBuilder::DEFAULT_CONTENT_TYPE,
34
- user_agent: RequestBuilder::DEFAULT_USER_AGENT,
35
- debug_output: nil,
36
- array_class: ResponseHandler::DEFAULT_ARRAY_CLASS,
37
- object_class: ResponseHandler::DEFAULT_OBJECT_CLASS,
34
+ array_class: nil,
35
+ object_class: nil,
38
36
  max_redirects: RedirectHandler::DEFAULT_MAX_REDIRECTS)
39
37
 
40
- initialize_authenticator(bearer_token, api_key, api_key_secret, access_token, access_token_secret)
41
- @connection = Connection.new(base_url: base_url, open_timeout: open_timeout, read_timeout: read_timeout,
38
+ initialize_oauth(api_key, api_key_secret, access_token, access_token_secret)
39
+ @bearer_token = bearer_token
40
+ initialize_authenticator
41
+ @base_url = base_url
42
+ @connection = Connection.new(open_timeout: open_timeout, read_timeout: read_timeout,
42
43
  write_timeout: write_timeout, debug_output: debug_output, proxy_url: proxy_url)
43
- @request_builder = RequestBuilder.new(content_type: content_type, user_agent: user_agent)
44
- @redirect_handler = RedirectHandler.new(@authenticator, @connection, @request_builder,
44
+ @request_builder = RequestBuilder.new
45
+ @redirect_handler = RedirectHandler.new(connection: @connection, request_builder: @request_builder,
45
46
  max_redirects: max_redirects)
46
- @response_handler = ResponseHandler.new(array_class: array_class, object_class: object_class)
47
+ @response_parser = ResponseParser.new(array_class: array_class, object_class: object_class)
48
+ end
49
+
50
+ def get(endpoint, headers: {})
51
+ execute_request(:get, endpoint, headers: headers)
52
+ end
53
+
54
+ def post(endpoint, body = nil, headers: {})
55
+ execute_request(:post, endpoint, body: body, headers: headers)
47
56
  end
48
57
 
49
- def get(endpoint)
50
- send_request(:get, endpoint)
58
+ def put(endpoint, body = nil, headers: {})
59
+ execute_request(:put, endpoint, body: body, headers: headers)
51
60
  end
52
61
 
53
- def post(endpoint, body = nil)
54
- send_request(:post, endpoint, body)
62
+ def delete(endpoint, headers: {})
63
+ execute_request(:delete, endpoint, headers: headers)
55
64
  end
56
65
 
57
- def put(endpoint, body = nil)
58
- send_request(:put, endpoint, body)
66
+ def api_key=(api_key)
67
+ @api_key = api_key
68
+ initialize_authenticator
59
69
  end
60
70
 
61
- def delete(endpoint)
62
- send_request(:delete, endpoint)
71
+ def api_key_secret=(api_key_secret)
72
+ @api_key_secret = api_key_secret
73
+ initialize_authenticator
74
+ end
75
+
76
+ def access_token=(access_token)
77
+ @access_token = access_token
78
+ initialize_authenticator
79
+ end
80
+
81
+ def access_token_secret=(access_token_secret)
82
+ @access_token_secret = access_token_secret
83
+ initialize_authenticator
84
+ end
85
+
86
+ def bearer_token=(bearer_token)
87
+ @bearer_token = bearer_token
88
+ initialize_authenticator
63
89
  end
64
90
 
65
91
  private
66
92
 
67
- def initialize_authenticator(bearer_token, api_key, api_key_secret, access_token, access_token_secret)
68
- @authenticator = if bearer_token
69
- BearerTokenAuthenticator.new(bearer_token)
70
- elsif api_key && api_key_secret && access_token && access_token_secret
71
- OauthAuthenticator.new(api_key, api_key_secret, access_token, access_token_secret)
93
+ def initialize_oauth(api_key, api_key_secret, access_token, access_token_secret)
94
+ @api_key = api_key
95
+ @api_key_secret = api_key_secret
96
+ @access_token = access_token
97
+ @access_token_secret = access_token_secret
98
+ end
99
+
100
+ def initialize_authenticator
101
+ @authenticator = if api_key && api_key_secret && access_token && access_token_secret
102
+ OAuthAuthenticator.new(api_key: api_key, api_key_secret: api_key_secret, access_token: access_token,
103
+ access_token_secret: access_token_secret)
104
+ elsif bearer_token
105
+ BearerTokenAuthenticator.new(bearer_token: bearer_token)
106
+ elsif @authenticator.nil?
107
+ Authenticator.new
72
108
  else
73
- raise ArgumentError,
74
- "Client must be initialized with either a bearer_token or " \
75
- "an api_key, api_key_secret, access_token, and access_token_secret"
109
+ @authenticator
76
110
  end
77
111
  end
78
112
 
79
- def send_request(http_method, endpoint, body = nil)
80
- uri = URI.join(base_uri.to_s, endpoint)
81
- request = @request_builder.build(@authenticator, http_method, uri, body: body)
82
- response = @connection.send_request(request)
83
- final_response = @redirect_handler.handle_redirects(response, request, base_uri)
84
- @response_handler.handle(final_response)
113
+ def execute_request(http_method, endpoint, headers:, body: nil)
114
+ uri = URI.join(base_url, endpoint)
115
+ request = @request_builder.build(http_method: http_method, uri: uri, body: body, headers: headers,
116
+ authenticator: @authenticator)
117
+ response = @connection.perform(request: request)
118
+ response = @redirect_handler.handle(response: response, request: request, base_url: base_url,
119
+ authenticator: @authenticator)
120
+ @response_parser.parse(response: response)
85
121
  end
86
122
  end
87
123
  end
data/lib/x/connection.rb CHANGED
@@ -9,12 +9,12 @@ module X
9
9
  class Connection
10
10
  extend Forwardable
11
11
 
12
- DEFAULT_BASE_URL = "https://api.twitter.com/2/".freeze
13
- DEFAULT_HOST = "https://api.twitter.com".freeze
12
+ DEFAULT_HOST = "api.twitter.com".freeze
14
13
  DEFAULT_PORT = 443
15
14
  DEFAULT_OPEN_TIMEOUT = 60 # seconds
16
15
  DEFAULT_READ_TIMEOUT = 60 # seconds
17
16
  DEFAULT_WRITE_TIMEOUT = 60 # seconds
17
+ DEFAULT_DEBUG_OUTPUT = File.open(File::NULL, "w")
18
18
  NETWORK_ERRORS = [
19
19
  Errno::ECONNREFUSED,
20
20
  Errno::ECONNRESET,
@@ -23,95 +23,58 @@ module X
23
23
  OpenSSL::SSL::SSLError
24
24
  ].freeze
25
25
 
26
- attr_reader :base_uri, :proxy_uri, :http_client
27
-
28
- def_delegators :@http_client, :open_timeout, :read_timeout, :write_timeout
29
- def_delegators :@http_client, :open_timeout=, :read_timeout=, :write_timeout=
30
- def_delegator :@http_client, :set_debug_output, :debug_output=
31
-
32
- def initialize(base_url: DEFAULT_BASE_URL, open_timeout: DEFAULT_OPEN_TIMEOUT,
33
- read_timeout: DEFAULT_READ_TIMEOUT, write_timeout: DEFAULT_WRITE_TIMEOUT, proxy_url: nil, debug_output: nil)
34
- @proxy_uri = URI(proxy_url) unless proxy_url.nil?
35
- self.base_uri = base_url
36
- apply_http_client_settings(
37
- open_timeout: open_timeout,
38
- read_timeout: read_timeout,
39
- write_timeout: write_timeout,
40
- debug_output: debug_output
41
- )
26
+ attr_accessor :open_timeout, :read_timeout, :write_timeout, :debug_output
27
+ attr_reader :proxy_url, :proxy_uri
28
+
29
+ def_delegator :proxy_uri, :host, :proxy_host
30
+ def_delegator :proxy_uri, :port, :proxy_port
31
+ def_delegator :proxy_uri, :user, :proxy_user
32
+ def_delegator :proxy_uri, :password, :proxy_pass
33
+
34
+ def initialize(open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT,
35
+ write_timeout: DEFAULT_WRITE_TIMEOUT, debug_output: DEFAULT_DEBUG_OUTPUT, proxy_url: nil)
36
+ @open_timeout = open_timeout
37
+ @read_timeout = read_timeout
38
+ @write_timeout = write_timeout
39
+ @debug_output = debug_output
40
+ self.proxy_url = proxy_url unless proxy_url.nil?
42
41
  end
43
42
 
44
- def send_request(request)
45
- response = @http_client.request(request)
43
+ def perform(request:)
44
+ host = request.uri.host || DEFAULT_HOST
45
+ port = request.uri.port || DEFAULT_PORT
46
+ http_client = build_http_client(host, port)
47
+ http_client.use_ssl = request.uri.scheme.eql?("https")
48
+ http_client.request(request)
46
49
  rescue *NETWORK_ERRORS => e
47
- raise NetworkError.new("Network error: #{e.message}", response: response)
50
+ raise NetworkError, "Network error: #{e}"
48
51
  end
49
52
 
50
- def base_uri=(base_url)
51
- base_uri = URI(base_url)
52
- raise ArgumentError, "Invalid base URL" unless base_uri.is_a?(URI::HTTPS) || base_uri.is_a?(URI::HTTP)
53
-
54
- @base_uri = base_uri
55
- update_http_client_settings
56
- end
53
+ def proxy_url=(proxy_url)
54
+ @proxy_url = proxy_url
55
+ proxy_uri = URI(proxy_url)
56
+ raise ArgumentError, "Invalid proxy URL: #{proxy_uri}" unless proxy_uri.is_a?(URI::HTTP)
57
57
 
58
- def debug_output
59
- @http_client.instance_variable_get(:@debug_output)
60
- end
61
-
62
- def configuration
63
- {
64
- base_url: base_uri.to_s,
65
- open_timeout: open_timeout,
66
- read_timeout: read_timeout,
67
- write_timeout: write_timeout,
68
- proxy_url: proxy_uri.to_s,
69
- debug_output: debug_output
70
- }
58
+ @proxy_uri = proxy_uri
71
59
  end
72
60
 
73
61
  private
74
62
 
75
- def apply_http_client_settings(open_timeout:, read_timeout:, write_timeout:, debug_output:)
76
- @http_client.open_timeout = open_timeout
77
- @http_client.read_timeout = read_timeout
78
- @http_client.write_timeout = write_timeout
79
- @http_client.set_debug_output(debug_output) if debug_output
80
- end
81
-
82
- def current_http_client_settings
83
- {
84
- open_timeout: @http_client.open_timeout,
85
- read_timeout: @http_client.read_timeout,
86
- write_timeout: @http_client.write_timeout,
87
- debug_output: debug_output
88
- }
89
- end
90
-
91
- def update_http_client_settings
92
- conditionally_apply_http_client_settings do
93
- host = @base_uri.host || DEFAULT_HOST
94
- port = @base_uri.port || DEFAULT_PORT
95
- @http_client = build_http_client(host: host, port: port)
96
- @http_client.use_ssl = @base_uri.scheme == "https"
97
- end
98
- end
99
-
100
- def build_http_client(host:, port:)
101
- if defined?(@proxy_uri)
102
- Net::HTTP.new(host, port, @proxy_uri&.host, @proxy_uri&.port, @proxy_uri&.user, @proxy_uri&.password)
63
+ def build_http_client(host = DEFAULT_HOST, port = DEFAULT_PORT)
64
+ http_client = if proxy_uri
65
+ Net::HTTP.new(host, port, proxy_host, proxy_port, proxy_user, proxy_pass)
103
66
  else
104
67
  Net::HTTP.new(host, port)
105
68
  end
69
+ configure_http_client(http_client)
106
70
  end
107
71
 
108
- def conditionally_apply_http_client_settings
109
- if defined?(@http_client)
110
- settings = current_http_client_settings
111
- yield
112
- apply_http_client_settings(**settings)
113
- else
114
- yield
72
+ def configure_http_client(http_client)
73
+ http_client.tap do |c|
74
+ c.open_timeout = open_timeout
75
+ c.read_timeout = read_timeout
76
+ c.write_timeout = write_timeout
77
+ c.set_debug_output(debug_output)
115
78
  end
116
79
  end
117
80
  end
@@ -0,0 +1,5 @@
1
+ require_relative "server_error"
2
+
3
+ module X
4
+ class BadGateway < ServerError; end
5
+ end
@@ -1,5 +1,5 @@
1
1
  require_relative "client_error"
2
2
 
3
3
  module X
4
- class NotFoundError < ClientError; end
4
+ class BadRequest < ClientError; end
5
5
  end
@@ -1,5 +1,5 @@
1
- require_relative "error"
1
+ require_relative "http_error"
2
2
 
3
3
  module X
4
- class ClientError < Error; end
4
+ class ClientError < HTTPError; end
5
5
  end
@@ -0,0 +1,5 @@
1
+ require_relative "client_error"
2
+
3
+ module X
4
+ class ConnectionException < ClientError; end
5
+ end
@@ -1,13 +1,3 @@
1
- require "json"
2
-
3
1
  module X
4
- # Base error class
5
- class Error < ::StandardError
6
- attr_reader :object
7
-
8
- def initialize(msg, response:)
9
- @object = JSON.parse(response.body) if response&.body && !response.body.empty?
10
- super(msg)
11
- end
12
- end
2
+ class Error < StandardError; end
13
3
  end
@@ -1,5 +1,5 @@
1
1
  require_relative "client_error"
2
2
 
3
3
  module X
4
- class ForbiddenError < ClientError; end
4
+ class Forbidden < ClientError; end
5
5
  end
@@ -0,0 +1,5 @@
1
+ require_relative "server_error"
2
+
3
+ module X
4
+ class GatewayTimeout < ServerError; end
5
+ end
@@ -1,5 +1,5 @@
1
1
  require_relative "client_error"
2
2
 
3
3
  module X
4
- class BadRequestError < ClientError; end
4
+ class Gone < ClientError; end
5
5
  end
@@ -0,0 +1,42 @@
1
+ require "json"
2
+ require_relative "error"
3
+
4
+ module X
5
+ # Base HTTP error class
6
+ class HTTPError < Error
7
+ JSON_CONTENT_TYPE_REGEXP = %r{application/(problem\+|)json}
8
+
9
+ attr_reader :response, :code
10
+
11
+ def initialize(response:)
12
+ super(error_message(response))
13
+ @response = response
14
+ @code = response.code
15
+ end
16
+
17
+ def error_message(response)
18
+ if json?(response)
19
+ message_from_json_response(response)
20
+ else
21
+ response.message
22
+ end
23
+ end
24
+
25
+ def message_from_json_response(response)
26
+ response_object = JSON.parse(response.body)
27
+ if response_object.key?("title") && response_object.key?("detail")
28
+ "#{response_object.fetch("title")}: #{response_object.fetch("detail")}"
29
+ elsif response_object.key?("error")
30
+ response_object.fetch("error")
31
+ elsif response_object["errors"].instance_of?(Array)
32
+ response_object.fetch("errors").map { |error| error.fetch("message") }.join(", ")
33
+ else
34
+ response.message
35
+ end
36
+ end
37
+
38
+ def json?(response)
39
+ JSON_CONTENT_TYPE_REGEXP.match?(response["content-type"])
40
+ end
41
+ end
42
+ end
@@ -1,4 +1,4 @@
1
- require_relative "server_error"
1
+ require_relative "error"
2
2
 
3
3
  module X
4
4
  class NetworkError < Error; end
@@ -0,0 +1,5 @@
1
+ require_relative "client_error"
2
+
3
+ module X
4
+ class NotAcceptable < ClientError; end
5
+ end
@@ -0,0 +1,5 @@
1
+ require_relative "client_error"
2
+
3
+ module X
4
+ class NotFound < ClientError; end
5
+ end