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
@@ -0,0 +1,5 @@
1
+ require_relative "client_error"
2
+
3
+ module X
4
+ class PayloadTooLarge < ClientError; end
5
+ end
@@ -1,5 +1,5 @@
1
- require_relative "error"
1
+ require_relative "http_error"
2
2
 
3
3
  module X
4
- class ServerError < Error; end
4
+ class ServerError < HTTPError; end
5
5
  end
@@ -0,0 +1,5 @@
1
+ require_relative "server_error"
2
+
3
+ module X
4
+ class ServiceUnavailable < ServerError; end
5
+ end
@@ -0,0 +1,5 @@
1
+ require_relative "error"
2
+
3
+ module X
4
+ class TooManyRedirects < Error; end
5
+ end
@@ -0,0 +1,24 @@
1
+ require_relative "client_error"
2
+
3
+ module X
4
+ # Rate limit error class
5
+ class TooManyRequests < ClientError
6
+ def limit
7
+ response["x-rate-limit-limit"].to_i
8
+ end
9
+
10
+ def remaining
11
+ response["x-rate-limit-remaining"].to_i
12
+ end
13
+
14
+ def reset_at
15
+ Time.at(response["x-rate-limit-reset"].to_i)
16
+ end
17
+
18
+ def reset_in
19
+ [(reset_at - Time.now).ceil, 0].max
20
+ end
21
+
22
+ alias_method :retry_after, :reset_in
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ require_relative "client_error"
2
+
3
+ module X
4
+ class Unauthorized < ClientError; end
5
+ end
@@ -0,0 +1,5 @@
1
+ require_relative "client_error"
2
+
3
+ module X
4
+ class UnprocessableEntity < ClientError; end
5
+ end
@@ -2,7 +2,7 @@ require "securerandom"
2
2
 
3
3
  module X
4
4
  # Helper module for uploading images and videos
5
- module MediaUpload
5
+ module MediaUploader
6
6
  extend self
7
7
 
8
8
  MAX_RETRIES = 3
@@ -15,28 +15,29 @@ module X
15
15
  MIME_TYPE_MAP = {"gif" => GIF_MIME_TYPE, "jpg" => JPEG_MIME_TYPE, "jpeg" => JPEG_MIME_TYPE, "mp4" => MP4_MIME_TYPE,
16
16
  "png" => PNG_MIME_TYPE, "srt" => SUBRIP_MIME_TYPE, "webp" => WEBP_MIME_TYPE}.freeze
17
17
 
18
- def media_upload(client:, file_path:, media_category:, media_type: infer_media_type(file_path, media_category),
18
+ def upload(client:, file_path:, media_category:, media_type: infer_media_type(file_path, media_category),
19
19
  boundary: SecureRandom.hex)
20
20
  validate!(file_path: file_path, media_category: media_category)
21
- upload_client = client.dup.tap { |c| c.base_uri = "https://upload.twitter.com/1.1/" }
22
- upload_body = construct_upload_body(file_path, media_type, boundary)
23
- upload_client.content_type = "multipart/form-data, boundary=#{boundary}"
24
- upload_client.post("media/upload.json?media_category=#{media_category}", upload_body)
21
+ upload_client = client.dup.tap { |c| c.base_url = "https://upload.twitter.com/1.1/" }
22
+ upload_body = construct_upload_body(file_path: file_path, media_type: media_type, boundary: boundary)
23
+ headers = {"Content-Type" => "multipart/form-data, boundary=#{boundary}"}
24
+ upload_client.post("media/upload.json?media_category=#{media_category}", upload_body, headers: headers)
25
25
  end
26
26
 
27
- def chunked_media_upload(client:, file_path:, media_category:, media_type: infer_media_type(file_path,
27
+ def chunked_upload(client:, file_path:, media_category:, media_type: infer_media_type(file_path,
28
28
  media_category), boundary: SecureRandom.hex, chunk_size_mb: 8)
29
29
  validate!(file_path: file_path, media_category: media_category)
30
- upload_client = client.dup.tap { |c| c.base_uri = "https://upload.twitter.com/1.1/" }
31
- media = init(upload_client, file_path, media_type, media_category)
30
+ upload_client = client.dup.tap { |c| c.base_url = "https://upload.twitter.com/1.1/" }
31
+ media = init(upload_client: upload_client, file_path: file_path, media_type: media_type,
32
+ media_category: media_category)
32
33
  chunk_size = chunk_size_mb * BYTES_PER_MB
33
- chunk_paths = split(file_path, chunk_size)
34
- append(upload_client, chunk_paths, media, media_type, boundary)
34
+ append(upload_client: upload_client, file_paths: split(file_path, chunk_size), media: media,
35
+ media_type: media_type, boundary: boundary)
35
36
  upload_client.post("media/upload.json?command=FINALIZE&media_id=#{media["media_id"]}")
36
37
  end
37
38
 
38
39
  def await_processing(client:, media:)
39
- upload_client = client.dup.tap { |c| c.base_uri = "https://upload.twitter.com/1.1/" }
40
+ upload_client = client.dup.tap { |c| c.base_url = "https://upload.twitter.com/1.1/" }
40
41
  loop do
41
42
  status = upload_client.get("media/upload.json?command=STATUS&media_id=#{media["media_id"]}")
42
43
  return status if status["processing_info"]["state"] == "succeeded"
@@ -64,19 +65,13 @@ module X
64
65
  end
65
66
  end
66
67
 
67
- def init(upload_client, file_path, media_type, media_category)
68
- total_bytes = File.size(file_path)
69
- query = "command=INIT&media_type=#{media_type}&media_category=#{media_category}&total_bytes=#{total_bytes}"
70
- upload_client.post("media/upload.json?#{query}")
71
- end
72
-
73
68
  def split(file_path, chunk_size)
74
69
  file_number = -1
75
70
 
76
- [].tap do |chunk_paths|
71
+ [].tap do |file_paths|
77
72
  File.open(file_path, "rb") do |f|
78
73
  while (chunk = f.read(chunk_size))
79
- chunk_paths << "#{Dir.mktmpdir}/x#{format("%03d", file_number += 1)}".tap do |path|
74
+ file_paths << "#{Dir.mktmpdir}/x#{format("%03d", file_number += 1)}".tap do |path|
80
75
  File.write(path, chunk)
81
76
  end
82
77
  end
@@ -84,37 +79,41 @@ module X
84
79
  end
85
80
  end
86
81
 
87
- def append(upload_client, chunk_paths, media, media_type, boundary = SecureRandom.hex)
88
- threads = chunk_paths.map.with_index do |chunk_path, index|
82
+ def init(upload_client:, file_path:, media_type:, media_category:)
83
+ total_bytes = File.size(file_path)
84
+ query = "command=INIT&media_type=#{media_type}&media_category=#{media_category}&total_bytes=#{total_bytes}"
85
+ upload_client.post("media/upload.json?#{query}")
86
+ end
87
+
88
+ def append(upload_client:, file_paths:, media:, media_type:, boundary: SecureRandom.hex)
89
+ threads = file_paths.map.with_index do |file_path, index|
89
90
  Thread.new do
90
- upload_body = construct_upload_body(chunk_path, media_type, boundary)
91
+ upload_body = construct_upload_body(file_path: file_path, media_type: media_type, boundary: boundary)
91
92
  query = "command=APPEND&media_id=#{media["media_id"]}&segment_index=#{index}"
92
- upload_chunk(upload_client, query, upload_body, chunk_path, boundary)
93
+ headers = {"Content-Type" => "multipart/form-data, boundary=#{boundary}"}
94
+ upload_chunk(upload_client: upload_client, query: query, upload_body: upload_body, file_path: file_path,
95
+ headers: headers)
93
96
  end
94
97
  end
95
98
  threads.each(&:join)
96
99
  end
97
100
 
98
- def upload_chunk(upload_client, query, upload_body, chunk_path, boundary)
99
- # Initialize a new client to avoid shared connection issues
100
- client = upload_client.dup
101
- client.connection = Connection.new(**upload_client.connection.configuration.merge(base_url: "https://upload.twitter.com/1.1/"))
102
- client.content_type = "multipart/form-data, boundary=#{boundary}"
103
- client.post("media/upload.json?#{query}", upload_body)
101
+ def upload_chunk(upload_client:, query:, upload_body:, file_path:, headers: {})
102
+ upload_client.post("media/upload.json?#{query}", upload_body, headers: headers)
104
103
  rescue NetworkError, ServerError
105
104
  retries ||= 0
106
105
  ((retries += 1) < MAX_RETRIES) ? retry : raise
107
106
  ensure
108
- cleanup_chunk(chunk_path)
107
+ cleanup_file(file_path)
109
108
  end
110
109
 
111
- def cleanup_chunk(chunk_path)
112
- dirname = File.dirname(chunk_path)
113
- File.delete(chunk_path)
110
+ def cleanup_file(file_path)
111
+ dirname = File.dirname(file_path)
112
+ File.delete(file_path)
114
113
  Dir.delete(dirname) if Dir.empty?(dirname)
115
114
  end
116
115
 
117
- def construct_upload_body(file_path, media_type, boundary = SecureRandom.hex)
116
+ def construct_upload_body(file_path:, media_type:, boundary: SecureRandom.hex)
118
117
  "--#{boundary}\r\n" \
119
118
  "Content-Disposition: form-data; name=\"media\"; filename=\"#{File.basename(file_path)}\"\r\n" \
120
119
  "Content-Type: #{media_type}\r\n\r\n" \
@@ -1,20 +1,21 @@
1
1
  require "base64"
2
- require "cgi"
3
2
  require "json"
4
3
  require "openssl"
5
4
  require "securerandom"
6
5
  require "uri"
6
+ require_relative "authenticator"
7
+ require_relative "cgi"
7
8
 
8
9
  module X
9
10
  # Handles OAuth authentication
10
- class OauthAuthenticator
11
+ class OAuthAuthenticator < Authenticator
11
12
  OAUTH_VERSION = "1.0".freeze
12
13
  OAUTH_SIGNATURE_METHOD = "HMAC-SHA1".freeze
13
14
  OAUTH_SIGNATURE_ALGORITHM = "sha1".freeze
14
15
 
15
16
  attr_accessor :api_key, :api_key_secret, :access_token, :access_token_secret
16
17
 
17
- def initialize(api_key, api_key_secret, access_token, access_token_secret)
18
+ def initialize(api_key:, api_key_secret:, access_token:, access_token_secret:) # rubocop:disable Lint/MissingSuper
18
19
  @api_key = api_key
19
20
  @api_key_secret = api_key_secret
20
21
  @access_token = access_token
@@ -23,7 +24,7 @@ module X
23
24
 
24
25
  def header(request)
25
26
  method, url, query_params = parse_request(request)
26
- build_oauth_header(method, url, query_params)
27
+ {AUTHENTICATION_HEADER => build_oauth_header(method, url, query_params)}
27
28
  end
28
29
 
29
30
  private
@@ -54,7 +55,7 @@ module X
54
55
  "oauth_consumer_key" => api_key,
55
56
  "oauth_nonce" => SecureRandom.hex,
56
57
  "oauth_signature_method" => OAUTH_SIGNATURE_METHOD,
57
- "oauth_timestamp" => Time.now.utc.to_i.to_s,
58
+ "oauth_timestamp" => Integer(Time.now).to_s,
58
59
  "oauth_token" => access_token,
59
60
  "oauth_version" => OAUTH_VERSION
60
61
  }
@@ -66,26 +67,20 @@ module X
66
67
  end
67
68
 
68
69
  def hmac_signature(base_string)
69
- digest = OpenSSL::Digest.new(OAUTH_SIGNATURE_ALGORITHM)
70
- hmac = OpenSSL::HMAC.digest(digest, signing_key, base_string)
70
+ hmac = OpenSSL::HMAC.digest(OAUTH_SIGNATURE_ALGORITHM, signing_key, base_string)
71
71
  Base64.strict_encode64(hmac)
72
72
  end
73
73
 
74
74
  def signature_base_string(method, url, params)
75
- "#{method}&#{escape(url)}&#{escape(URI.encode_www_form(params.sort))}"
75
+ "#{method}&#{CGI.escape(url)}&#{CGI.escape(CGI.escape_params(params.sort))}"
76
76
  end
77
77
 
78
78
  def signing_key
79
- "#{escape(api_key_secret)}&#{escape(access_token_secret)}"
79
+ "#{api_key_secret}&#{access_token_secret}"
80
80
  end
81
81
 
82
82
  def format_oauth_header(params)
83
- "OAuth #{params.sort.map { |k, v| "#{k}=\"#{escape(v.to_s)}\"" }.join(", ")}"
84
- end
85
-
86
- def escape(value)
87
- # TODO: Replace CGI.escape with CGI.escapeURIComponent when support for Ruby 3.1 is dropped
88
- CGI.escape(value.to_s).gsub("+", "%20")
83
+ "OAuth #{params.sort.map { |k, v| "#{k}=\"#{CGI.escape(v)}\"" }.join(", ")}"
89
84
  end
90
85
  end
91
86
  end
@@ -1,31 +1,35 @@
1
1
  require "net/http"
2
2
  require "uri"
3
+ require_relative "authenticator"
3
4
  require_relative "connection"
4
- require_relative "errors/too_many_redirects_error"
5
+ require_relative "errors/too_many_redirects"
6
+ require_relative "request_builder"
5
7
 
6
8
  module X
7
9
  # Handles HTTP redirects
8
10
  class RedirectHandler
9
11
  DEFAULT_MAX_REDIRECTS = 10
10
12
 
11
- attr_reader :authenticator, :connection, :request_builder, :max_redirects
13
+ attr_accessor :max_redirects
14
+ attr_reader :connection, :request_builder
12
15
 
13
- def initialize(authenticator, connection, request_builder, max_redirects: DEFAULT_MAX_REDIRECTS)
14
- @authenticator = authenticator
16
+ def initialize(connection: Connection.new, request_builder: RequestBuilder.new,
17
+ max_redirects: DEFAULT_MAX_REDIRECTS)
15
18
  @connection = connection
16
19
  @request_builder = request_builder
17
20
  @max_redirects = max_redirects
18
21
  end
19
22
 
20
- def handle_redirects(response, original_request, original_base_url, redirect_count = 0)
23
+ def handle(response:, request:, base_url:, authenticator: Authenticator.new, redirect_count: 0)
21
24
  if response.is_a?(Net::HTTPRedirection)
22
- raise TooManyRedirectsError.new("Too many redirects", response: response) if redirect_count >= max_redirects
25
+ raise TooManyRedirects, "Too many redirects" if redirect_count > max_redirects
23
26
 
24
- new_uri = build_new_uri(response, original_base_url)
25
- new_request = build_request(original_request, new_uri)
26
- new_response = send_new_request(new_uri, new_request)
27
+ new_uri = build_new_uri(response, base_url)
27
28
 
28
- handle_redirects(new_response, new_request, original_base_url, redirect_count + 1)
29
+ new_request = build_request(request, new_uri, Integer(response.code), authenticator)
30
+ new_response = connection.perform(request: new_request)
31
+
32
+ handle(response: new_response, request: new_request, base_url: base_url, redirect_count: redirect_count + 1)
29
33
  else
30
34
  response
31
35
  end
@@ -33,22 +37,21 @@ module X
33
37
 
34
38
  private
35
39
 
36
- def build_new_uri(response, original_base_url)
37
- location = response["location"].to_s
38
- new_uri = URI.parse(location)
39
- new_uri = URI.join(original_base_url.to_s, location) if new_uri.relative?
40
- new_uri
40
+ def build_new_uri(response, base_url)
41
+ location = response.fetch("location")
42
+ # If location is relative, it will join with the original base URL, otherwise it will overwrite it
43
+ URI.join(base_url, location)
41
44
  end
42
45
 
43
- def build_request(original_request, new_uri)
44
- http_method = original_request.method.downcase.to_sym
45
- body = original_request.body if original_request.body
46
- request_builder.build(authenticator, http_method, new_uri, body: body)
47
- end
46
+ def build_request(request, new_uri, response_code, authenticator)
47
+ http_method, body = case response_code
48
+ in 307 | 308
49
+ [request.method.downcase.to_sym, request.body]
50
+ else
51
+ [:get, nil]
52
+ end
48
53
 
49
- def send_new_request(new_uri, new_request)
50
- @connection = Connection.new(**connection.configuration.merge(base_url: new_uri))
51
- connection.send_request(new_request)
54
+ request_builder.build(http_method: http_method, uri: new_uri, body: body, authenticator: authenticator)
52
55
  end
53
56
  end
54
57
  end
@@ -1,71 +1,58 @@
1
1
  require "net/http"
2
2
  require "uri"
3
+ require_relative "authenticator"
4
+ require_relative "cgi"
3
5
  require_relative "version"
4
6
 
5
7
  module X
6
8
  # Creates HTTP requests
7
9
  class RequestBuilder
10
+ DEFAULT_HEADERS = {
11
+ "Content-Type" => "application/json; charset=utf-8",
12
+ "User-Agent" => "X-Client/#{VERSION} #{RUBY_ENGINE}/#{RUBY_VERSION} (#{RUBY_PLATFORM})"
13
+ }.freeze
8
14
  HTTP_METHODS = {
9
15
  get: Net::HTTP::Get,
10
16
  post: Net::HTTP::Post,
11
17
  put: Net::HTTP::Put,
12
18
  delete: Net::HTTP::Delete
13
19
  }.freeze
14
- DEFAULT_CONTENT_TYPE = "application/json; charset=utf-8".freeze
15
- DEFAULT_USER_AGENT = "X-Client/#{VERSION} #{RUBY_ENGINE}/#{RUBY_VERSION} (#{RUBY_PLATFORM})".freeze
16
- AUTHORIZATION_HEADER = "Authorization".freeze
17
- CONTENT_TYPE_HEADER = "Content-Type".freeze
18
- USER_AGENT_HEADER = "User-Agent".freeze
19
-
20
- attr_accessor :content_type, :user_agent
21
-
22
- def initialize(content_type: DEFAULT_CONTENT_TYPE, user_agent: DEFAULT_USER_AGENT)
23
- @content_type = content_type
24
- @user_agent = user_agent
25
- end
26
20
 
27
- def build(authenticator, http_method, uri, body: nil)
28
- request = create_request(http_method, uri, body)
29
- add_authorization(request, authenticator)
30
- add_content_type(request)
31
- add_user_agent(request)
21
+ def build(http_method:, uri:, body: nil, headers: {}, authenticator: Authenticator.new)
22
+ request = create_request(http_method: http_method, uri: uri, body: body)
23
+ add_headers(request: request, headers: headers)
24
+ add_authentication(request: request, authenticator: authenticator)
32
25
  request
33
26
  end
34
27
 
35
- def configuration
36
- {
37
- content_type: content_type,
38
- user_agent: user_agent
39
- }
40
- end
41
-
42
28
  private
43
29
 
44
- def create_request(http_method, uri, body)
30
+ def create_request(http_method:, uri:, body:)
45
31
  http_method_class = HTTP_METHODS[http_method]
46
32
 
47
33
  raise ArgumentError, "Unsupported HTTP method: #{http_method}" unless http_method_class
48
34
 
49
35
  escaped_uri = escape_query_params(uri)
50
36
  request = http_method_class.new(escaped_uri)
51
- request.body = body if body && http_method != :get
37
+ request.body = body
52
38
  request
53
39
  end
54
40
 
55
- def add_authorization(request, authenticator)
56
- request.add_field(AUTHORIZATION_HEADER, authenticator.header(request))
57
- end
58
-
59
- def add_content_type(request)
60
- request.add_field(CONTENT_TYPE_HEADER, content_type) if content_type
41
+ def add_authentication(request:, authenticator:)
42
+ authenticator.header(request).each do |key, value|
43
+ request.add_field(key, value)
44
+ end
61
45
  end
62
46
 
63
- def add_user_agent(request)
64
- request.add_field(USER_AGENT_HEADER, user_agent) if user_agent
47
+ def add_headers(request:, headers:)
48
+ DEFAULT_HEADERS.merge(headers).each do |key, value|
49
+ request.delete(key)
50
+ request.add_field(key, value)
51
+ end
65
52
  end
66
53
 
67
54
  def escape_query_params(uri)
68
- URI(uri).tap { |u| u.query = URI.encode_www_form(URI.decode_www_form(uri.query)) if uri.query }
55
+ URI(uri).tap { |u| u.query = CGI.escape_params(URI.decode_www_form(u.query)) if u.query }
69
56
  end
70
57
  end
71
58
  end
@@ -0,0 +1,69 @@
1
+ require "json"
2
+ require "net/http"
3
+ require_relative "errors/bad_gateway"
4
+ require_relative "errors/bad_request"
5
+ require_relative "errors/connection_exception"
6
+ require_relative "errors/http_error"
7
+ require_relative "errors/forbidden"
8
+ require_relative "errors/gateway_timeout"
9
+ require_relative "errors/gone"
10
+ require_relative "errors/internal_server_error"
11
+ require_relative "errors/not_acceptable"
12
+ require_relative "errors/not_found"
13
+ require_relative "errors/payload_too_large"
14
+ require_relative "errors/service_unavailable"
15
+ require_relative "errors/too_many_requests"
16
+ require_relative "errors/unauthorized"
17
+ require_relative "errors/unprocessable_entity"
18
+
19
+ module X
20
+ # Process HTTP responses
21
+ class ResponseParser
22
+ ERROR_MAP = {
23
+ 400 => BadRequest,
24
+ 401 => Unauthorized,
25
+ 403 => Forbidden,
26
+ 404 => NotFound,
27
+ 406 => NotAcceptable,
28
+ 409 => ConnectionException,
29
+ 410 => Gone,
30
+ 413 => PayloadTooLarge,
31
+ 422 => UnprocessableEntity,
32
+ 429 => TooManyRequests,
33
+ 500 => InternalServerError,
34
+ 502 => BadGateway,
35
+ 503 => ServiceUnavailable,
36
+ 504 => GatewayTimeout
37
+ }.freeze
38
+ JSON_CONTENT_TYPE_REGEXP = %r{application/json}
39
+
40
+ attr_accessor :array_class, :object_class
41
+
42
+ def initialize(array_class: nil, object_class: nil)
43
+ @array_class = array_class
44
+ @object_class = object_class
45
+ end
46
+
47
+ def parse(response:)
48
+ raise error(response) unless response.is_a?(Net::HTTPSuccess)
49
+
50
+ return unless json?(response)
51
+
52
+ JSON.parse(response.body, array_class: array_class, object_class: object_class)
53
+ end
54
+
55
+ private
56
+
57
+ def error(response)
58
+ error_class(response).new(response: response)
59
+ end
60
+
61
+ def error_class(response)
62
+ ERROR_MAP[Integer(response.code)] || HTTPError
63
+ end
64
+
65
+ def json?(response)
66
+ JSON_CONTENT_TYPE_REGEXP.match?(response["content-type"])
67
+ end
68
+ end
69
+ end
data/lib/x/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  require "rubygems/version"
2
2
 
3
3
  module X
4
- VERSION = Gem::Version.create("0.10.0")
4
+ VERSION = Gem::Version.create("0.12.0")
5
5
  end