x 0.9.1 → 0.11.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -27
- data/README.md +6 -2
- data/lib/x/authenticator.rb +8 -0
- data/lib/x/bearer_token_authenticator.rb +5 -3
- data/lib/x/cgi.rb +15 -0
- data/lib/x/client.rb +40 -37
- data/lib/x/connection.rb +42 -65
- 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/connection_exception.rb +5 -0
- data/lib/x/errors/error.rb +1 -9
- 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/internal_server_error.rb +5 -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/service_unavailable.rb +5 -0
- data/lib/x/errors/too_many_redirects.rb +5 -0
- data/lib/x/errors/too_many_requests.rb +29 -0
- data/lib/x/errors/unauthorized.rb +5 -0
- data/lib/x/errors/unprocessable_entity.rb +5 -0
- data/lib/x/media_uploader.rb +122 -0
- data/lib/x/oauth_authenticator.rb +10 -15
- data/lib/x/redirect_handler.rb +26 -24
- data/lib/x/request_builder.rb +22 -28
- data/lib/x/response_parser.rb +92 -0
- data/lib/x/version.rb +1 -1
- data/sig/x.rbs +160 -71
- metadata +22 -11
- data/lib/x/errors/authentication_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 -54
@@ -0,0 +1,122 @@
|
|
1
|
+
require "securerandom"
|
2
|
+
|
3
|
+
module X
|
4
|
+
# Helper module for uploading images and videos
|
5
|
+
module MediaUploader
|
6
|
+
extend self
|
7
|
+
|
8
|
+
MAX_RETRIES = 3
|
9
|
+
BYTES_PER_MB = 1_048_576
|
10
|
+
MEDIA_CATEGORIES = %w[dm_gif dm_image dm_video subtitles tweet_gif tweet_image tweet_video].freeze
|
11
|
+
DM_GIF, DM_IMAGE, DM_VIDEO, SUBTITLES, TWEET_GIF, TWEET_IMAGE, TWEET_VIDEO = MEDIA_CATEGORIES
|
12
|
+
DEFAULT_MIME_TYPE = "application/octet-stream".freeze
|
13
|
+
MIME_TYPES = %w[image/gif image/jpeg video/mp4 image/png application/x-subrip image/webp].freeze
|
14
|
+
GIF_MIME_TYPE, JPEG_MIME_TYPE, MP4_MIME_TYPE, PNG_MIME_TYPE, SUBRIP_MIME_TYPE, WEBP_MIME_TYPE = MIME_TYPES
|
15
|
+
MIME_TYPE_MAP = {"gif" => GIF_MIME_TYPE, "jpg" => JPEG_MIME_TYPE, "jpeg" => JPEG_MIME_TYPE, "mp4" => MP4_MIME_TYPE,
|
16
|
+
"png" => PNG_MIME_TYPE, "srt" => SUBRIP_MIME_TYPE, "webp" => WEBP_MIME_TYPE}.freeze
|
17
|
+
|
18
|
+
def upload(client:, file_path:, media_category:, media_type: infer_media_type(file_path, media_category),
|
19
|
+
boundary: SecureRandom.hex)
|
20
|
+
validate!(file_path: file_path, media_category: media_category)
|
21
|
+
upload_client = client.dup.tap { |c| c.base_url = "https://upload.twitter.com/1.1/" }
|
22
|
+
upload_body = construct_upload_body(file_path, media_type, 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
|
+
end
|
26
|
+
|
27
|
+
def chunked_upload(client:, file_path:, media_category:, media_type: infer_media_type(file_path,
|
28
|
+
media_category), boundary: SecureRandom.hex, chunk_size_mb: 8)
|
29
|
+
validate!(file_path: file_path, media_category: media_category)
|
30
|
+
upload_client = client.dup.tap { |c| c.base_url = "https://upload.twitter.com/1.1/" }
|
31
|
+
media = init(upload_client, file_path, media_type, media_category)
|
32
|
+
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)
|
35
|
+
upload_client.post("media/upload.json?command=FINALIZE&media_id=#{media["media_id"]}")
|
36
|
+
end
|
37
|
+
|
38
|
+
def await_processing(client:, media:)
|
39
|
+
upload_client = client.dup.tap { |c| c.base_url = "https://upload.twitter.com/1.1/" }
|
40
|
+
loop do
|
41
|
+
status = upload_client.get("media/upload.json?command=STATUS&media_id=#{media["media_id"]}")
|
42
|
+
return status if status["processing_info"]["state"] == "succeeded"
|
43
|
+
|
44
|
+
sleep status["processing_info"]["check_after_secs"].to_i
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def validate!(file_path:, media_category:)
|
51
|
+
raise "File not found: #{file_path}" unless File.exist?(file_path)
|
52
|
+
|
53
|
+
return if MEDIA_CATEGORIES.include?(media_category.downcase)
|
54
|
+
|
55
|
+
raise ArgumentError, "Invalid media_category: #{media_category}. Valid values: #{MEDIA_CATEGORIES.join(", ")}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def infer_media_type(file_path, media_category)
|
59
|
+
case media_category.downcase
|
60
|
+
when TWEET_GIF, DM_GIF then GIF_MIME_TYPE
|
61
|
+
when TWEET_VIDEO, DM_VIDEO then MP4_MIME_TYPE
|
62
|
+
when SUBTITLES then SUBRIP_MIME_TYPE
|
63
|
+
else MIME_TYPE_MAP[File.extname(file_path).delete(".").downcase] || DEFAULT_MIME_TYPE
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
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
|
+
def split(file_path, chunk_size)
|
74
|
+
file_number = -1
|
75
|
+
|
76
|
+
[].tap do |chunk_paths|
|
77
|
+
File.open(file_path, "rb") do |f|
|
78
|
+
while (chunk = f.read(chunk_size))
|
79
|
+
chunk_paths << "#{Dir.mktmpdir}/x#{format("%03d", file_number += 1)}".tap do |path|
|
80
|
+
File.write(path, chunk)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def append(upload_client, chunk_paths, media, media_type, boundary = SecureRandom.hex)
|
88
|
+
threads = chunk_paths.map.with_index do |chunk_path, index|
|
89
|
+
Thread.new do
|
90
|
+
upload_body = construct_upload_body(chunk_path, media_type, boundary)
|
91
|
+
query = "command=APPEND&media_id=#{media["media_id"]}&segment_index=#{index}"
|
92
|
+
headers = {"Content-Type" => "multipart/form-data, boundary=#{boundary}"}
|
93
|
+
upload_chunk(upload_client, query, upload_body, chunk_path, headers)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
threads.each(&:join)
|
97
|
+
end
|
98
|
+
|
99
|
+
def upload_chunk(upload_client, query, upload_body, chunk_path, headers = {})
|
100
|
+
upload_client.post("media/upload.json?#{query}", upload_body, headers: headers)
|
101
|
+
rescue NetworkError, ServerError
|
102
|
+
retries ||= 0
|
103
|
+
((retries += 1) < MAX_RETRIES) ? retry : raise
|
104
|
+
ensure
|
105
|
+
cleanup_chunk(chunk_path)
|
106
|
+
end
|
107
|
+
|
108
|
+
def cleanup_chunk(chunk_path)
|
109
|
+
dirname = File.dirname(chunk_path)
|
110
|
+
File.delete(chunk_path)
|
111
|
+
Dir.delete(dirname) if Dir.empty?(dirname)
|
112
|
+
end
|
113
|
+
|
114
|
+
def construct_upload_body(file_path, media_type, boundary = SecureRandom.hex)
|
115
|
+
"--#{boundary}\r\n" \
|
116
|
+
"Content-Disposition: form-data; name=\"media\"; filename=\"#{File.basename(file_path)}\"\r\n" \
|
117
|
+
"Content-Type: #{media_type}\r\n\r\n" \
|
118
|
+
"#{File.read(file_path)}\r\n" \
|
119
|
+
"--#{boundary}--\r\n"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -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
|
+
{"Authorization" => 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,36 @@
|
|
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 :authenticator, :connection, :request_builder
|
12
15
|
|
13
|
-
def initialize(authenticator, connection, request_builder
|
16
|
+
def initialize(authenticator: Authenticator.new, connection: Connection.new, request_builder: RequestBuilder.new,
|
17
|
+
max_redirects: DEFAULT_MAX_REDIRECTS)
|
14
18
|
@authenticator = authenticator
|
15
19
|
@connection = connection
|
16
20
|
@request_builder = request_builder
|
17
21
|
@max_redirects = max_redirects
|
18
22
|
end
|
19
23
|
|
20
|
-
def
|
24
|
+
def handle(response:, request:, base_url:, redirect_count: 0)
|
21
25
|
if response.is_a?(Net::HTTPRedirection)
|
22
|
-
raise
|
26
|
+
raise TooManyRedirects, "Too many redirects" if redirect_count > max_redirects
|
23
27
|
|
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)
|
28
|
+
new_uri = build_new_uri(response, base_url)
|
27
29
|
|
28
|
-
|
30
|
+
new_request = build_request(request, new_uri, Integer(response.code))
|
31
|
+
new_response = connection.perform(request: new_request)
|
32
|
+
|
33
|
+
handle(response: new_response, request: new_request, base_url: base_url, redirect_count: redirect_count + 1)
|
29
34
|
else
|
30
35
|
response
|
31
36
|
end
|
@@ -33,24 +38,21 @@ module X
|
|
33
38
|
|
34
39
|
private
|
35
40
|
|
36
|
-
def build_new_uri(response,
|
37
|
-
location = response
|
38
|
-
|
39
|
-
|
40
|
-
new_uri
|
41
|
+
def build_new_uri(response, base_url)
|
42
|
+
location = response.fetch("location")
|
43
|
+
# If location is relative, it will join with the original base URL, otherwise it will overwrite it
|
44
|
+
URI.join(base_url, location)
|
41
45
|
end
|
42
46
|
|
43
|
-
def build_request(
|
44
|
-
http_method =
|
45
|
-
|
46
|
-
|
47
|
-
|
47
|
+
def build_request(request, new_uri, response_code)
|
48
|
+
http_method, body = case response_code
|
49
|
+
in 307 | 308
|
50
|
+
[request.method.downcase.to_sym, request.body]
|
51
|
+
else
|
52
|
+
[:get, nil]
|
53
|
+
end
|
48
54
|
|
49
|
-
|
50
|
-
@connection = Connection.new(base_url: new_uri, open_timeout: connection.open_timeout,
|
51
|
-
read_timeout: connection.read_timeout, write_timeout: connection.write_timeout,
|
52
|
-
debug_output: connection.debug_output, proxy_url: connection.proxy_uri)
|
53
|
-
connection.send_request(new_request)
|
55
|
+
request_builder.build(http_method: http_method, uri: new_uri, body: body, authenticator: authenticator)
|
54
56
|
end
|
55
57
|
end
|
56
58
|
end
|
data/lib/x/request_builder.rb
CHANGED
@@ -1,64 +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
28
|
private
|
36
29
|
|
37
|
-
def create_request(http_method
|
30
|
+
def create_request(http_method:, uri:, body:)
|
38
31
|
http_method_class = HTTP_METHODS[http_method]
|
39
32
|
|
40
33
|
raise ArgumentError, "Unsupported HTTP method: #{http_method}" unless http_method_class
|
41
34
|
|
42
35
|
escaped_uri = escape_query_params(uri)
|
43
36
|
request = http_method_class.new(escaped_uri)
|
44
|
-
request.body = body
|
37
|
+
request.body = body
|
45
38
|
request
|
46
39
|
end
|
47
40
|
|
48
|
-
def
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
def add_content_type(request)
|
53
|
-
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
|
54
45
|
end
|
55
46
|
|
56
|
-
def
|
57
|
-
|
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
|
58
52
|
end
|
59
53
|
|
60
54
|
def escape_query_params(uri)
|
61
|
-
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 }
|
62
56
|
end
|
63
57
|
end
|
64
58
|
end
|
@@ -0,0 +1,92 @@
|
|
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/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_CLASSES = {
|
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/(problem\+|)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 success?(response)
|
49
|
+
|
50
|
+
JSON.parse(response.body, array_class: array_class, object_class: object_class) if json?(response)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def success?(response)
|
56
|
+
response.is_a?(Net::HTTPSuccess)
|
57
|
+
end
|
58
|
+
|
59
|
+
def error(response)
|
60
|
+
error_class(response).new(error_message(response), response)
|
61
|
+
end
|
62
|
+
|
63
|
+
def error_class(response)
|
64
|
+
ERROR_CLASSES[Integer(response.code)] || Error
|
65
|
+
end
|
66
|
+
|
67
|
+
def error_message(response)
|
68
|
+
if json?(response)
|
69
|
+
message_from_json_response(response)
|
70
|
+
else
|
71
|
+
response.message
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def message_from_json_response(response)
|
76
|
+
response_object = JSON.parse(response.body)
|
77
|
+
if response_object.key?("title") && response_object.key?("detail")
|
78
|
+
"#{response_object.fetch("title")}: #{response_object.fetch("detail")}"
|
79
|
+
elsif response_object.key?("error")
|
80
|
+
response_object.fetch("error")
|
81
|
+
elsif response_object["errors"].instance_of?(Array)
|
82
|
+
response_object.fetch("errors").map { |error| error.fetch("message") }.join(", ")
|
83
|
+
else
|
84
|
+
response.message
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def json?(response)
|
89
|
+
response.body && JSON_CONTENT_TYPE_REGEXP.match?(response["content-type"])
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/x/version.rb
CHANGED