x 0.10.0 → 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +47 -28
- data/README.md +12 -2
- data/lib/x/authenticator.rb +10 -0
- data/lib/x/bearer_token_authenticator.rb +5 -3
- data/lib/x/cgi.rb +15 -0
- data/lib/x/client.rb +85 -49
- data/lib/x/connection.rb +39 -76
- data/lib/x/errors/bad_gateway.rb +5 -0
- data/lib/x/errors/{not_found_error.rb → bad_request.rb} +1 -1
- data/lib/x/errors/client_error.rb +2 -2
- data/lib/x/errors/connection_exception.rb +5 -0
- data/lib/x/errors/error.rb +1 -11
- data/lib/x/errors/{forbidden_error.rb → forbidden.rb} +1 -1
- data/lib/x/errors/gateway_timeout.rb +5 -0
- data/lib/x/errors/{bad_request_error.rb → gone.rb} +1 -1
- data/lib/x/errors/http_error.rb +42 -0
- data/lib/x/errors/network_error.rb +1 -1
- data/lib/x/errors/not_acceptable.rb +5 -0
- data/lib/x/errors/not_found.rb +5 -0
- data/lib/x/errors/payload_too_large.rb +5 -0
- data/lib/x/errors/server_error.rb +2 -2
- data/lib/x/errors/service_unavailable.rb +5 -0
- data/lib/x/errors/too_many_redirects.rb +5 -0
- data/lib/x/errors/too_many_requests.rb +24 -0
- data/lib/x/errors/unauthorized.rb +5 -0
- data/lib/x/errors/unprocessable_entity.rb +5 -0
- data/lib/x/{media_upload.rb → media_uploader.rb} +34 -35
- data/lib/x/oauth_authenticator.rb +10 -15
- data/lib/x/redirect_handler.rb +26 -23
- data/lib/x/request_builder.rb +22 -35
- data/lib/x/response_parser.rb +69 -0
- data/lib/x/version.rb +1 -1
- data/sig/x.rbs +118 -92
- metadata +22 -13
- data/lib/x/errors/authentication_error.rb +0 -5
- data/lib/x/errors/payload_too_large_error.rb +0 -5
- data/lib/x/errors/service_unavailable_error.rb +0 -5
- data/lib/x/errors/too_many_redirects_error.rb +0 -5
- data/lib/x/errors/too_many_requests_error.rb +0 -29
- data/lib/x/response_handler.rb +0 -63
@@ -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
|
@@ -2,7 +2,7 @@ require "securerandom"
|
|
2
2
|
|
3
3
|
module X
|
4
4
|
# Helper module for uploading images and videos
|
5
|
-
module
|
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
|
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.
|
22
|
-
upload_body = construct_upload_body(file_path, media_type, boundary)
|
23
|
-
|
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
|
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.
|
31
|
-
media = init(upload_client, file_path, media_type,
|
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
|
-
|
34
|
-
|
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.
|
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 |
|
71
|
+
[].tap do |file_paths|
|
77
72
|
File.open(file_path, "rb") do |f|
|
78
73
|
while (chunk = f.read(chunk_size))
|
79
|
-
|
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
|
88
|
-
|
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(
|
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
|
-
|
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
|
99
|
-
|
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
|
-
|
107
|
+
cleanup_file(file_path)
|
109
108
|
end
|
110
109
|
|
111
|
-
def
|
112
|
-
dirname = File.dirname(
|
113
|
-
File.delete(
|
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
|
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
|
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
|
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.
|
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
|
-
|
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(
|
75
|
+
"#{method}&#{CGI.escape(url)}&#{CGI.escape(CGI.escape_params(params.sort))}"
|
76
76
|
end
|
77
77
|
|
78
78
|
def signing_key
|
79
|
-
"#{
|
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
|
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
|
data/lib/x/redirect_handler.rb
CHANGED
@@ -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/
|
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
|
-
|
13
|
+
attr_accessor :max_redirects
|
14
|
+
attr_reader :connection, :request_builder
|
12
15
|
|
13
|
-
def initialize(
|
14
|
-
|
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
|
23
|
+
def handle(response:, request:, base_url:, authenticator: Authenticator.new, redirect_count: 0)
|
21
24
|
if response.is_a?(Net::HTTPRedirection)
|
22
|
-
raise
|
25
|
+
raise TooManyRedirects, "Too many redirects" if redirect_count > max_redirects
|
23
26
|
|
24
|
-
new_uri = build_new_uri(response,
|
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
|
-
|
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,
|
37
|
-
location = response
|
38
|
-
|
39
|
-
|
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(
|
44
|
-
http_method =
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
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
|
data/lib/x/request_builder.rb
CHANGED
@@ -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(
|
28
|
-
request = create_request(http_method, uri, body)
|
29
|
-
|
30
|
-
|
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
|
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
|
37
|
+
request.body = body
|
52
38
|
request
|
53
39
|
end
|
54
40
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
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
|
64
|
-
|
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 =
|
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