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
@@ -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