x 0.10.0 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
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