Class: TwelvedataRuby::Response

Inherits:
Object
  • Object
show all
Defined in:
lib/twelvedata_ruby/response.rb

Constant Summary collapse

CSV_COL_SEP =

CSV column separator used by Twelve Data

";"
BODY_MAX_BYTESIZE =

Maximum response body size to keep in memory

16_000
HTTP_STATUSES =

HTTP status code ranges

{
  http_error: (400..600),
  success: (200..299),
}.freeze
CONTENT_TYPE_HANDLERS =

Content type handlers for different response formats

{
  json: { parser: :parse_json, dumper: :dump_json },
  csv: { parser: :parse_csv, dumper: :dump_csv },
  plain: { parser: :parse_plain, dumper: :to_s },
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(http_response:, request:) ⇒ Response

Initialize response with HTTP response and request

Parameters:

  • http_response (HTTPX::Response)

    HTTP response object

  • request (Request)

    Original request object



100
101
102
103
104
105
106
# File 'lib/twelvedata_ruby/response.rb', line 100

def initialize(http_response:, request:)
  @http_response = http_response
  @request = request
  @headers = http_response.headers
  @body_bytesize = http_response.body.bytesize
  @parsed_body = nil
end

Instance Attribute Details

#body_bytesizeObject (readonly)

Returns the value of attribute body_bytesize.



94
95
96
# File 'lib/twelvedata_ruby/response.rb', line 94

def body_bytesize
  @body_bytesize
end

#headersObject (readonly)

Returns the value of attribute headers.



94
95
96
# File 'lib/twelvedata_ruby/response.rb', line 94

def headers
  @headers
end

#http_responseObject (readonly)

Returns the value of attribute http_response.



94
95
96
# File 'lib/twelvedata_ruby/response.rb', line 94

def http_response
  @http_response
end

#requestObject (readonly)

Returns the value of attribute request.



94
95
96
# File 'lib/twelvedata_ruby/response.rb', line 94

def request
  @request
end

Class Method Details

.create_error_from_response(http_response, request) ⇒ Object (private)



62
63
64
65
66
# File 'lib/twelvedata_ruby/response.rb', line 62

def create_error_from_response(http_response, request)
  json_data = extract_error_data(http_response)
  error_class = determine_error_class(http_response.status)
  error_class.new(json_data:, request:, status_code: http_response.status, message: json_data[:message])
end

.determine_error_class(status_code) ⇒ Object (private)



82
83
84
85
86
87
88
89
90
91
# File 'lib/twelvedata_ruby/response.rb', line 82

def determine_error_class(status_code)
  error_class_name = ResponseError.error_class_for_code(status_code, :http) ||
                    ResponseError.error_class_for_code(status_code, :api)

  if error_class_name
    TwelvedataRuby.const_get(error_class_name)
  else
    ResponseError
  end
end

.extract_error_data(http_response) ⇒ Object (private)



68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/twelvedata_ruby/response.rb', line 68

def extract_error_data(http_response)
  if http_response.respond_to?(:error) && http_response.error
    {
      message: http_response.error.message,
      code: http_response.error.class.name,
    }
  else
    {
      message: http_response.body.to_s,
      code: http_response.status,
    }
  end
end

.resolve(http_response, request) ⇒ Response, ResponseError

Resolve HTTP response into Response or ResponseError

Parameters:

  • http_response (HTTPX::Response)

    HTTP response from client

  • request (Request)

    Original request object

Returns:



35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/twelvedata_ruby/response.rb', line 35

def resolve(http_response, request)
  if success_status?(http_response.status)
    new(http_response: http_response, request: request)
  else
    create_error_from_response(http_response, request)
  end
rescue StandardError => e
  ResponseError.new(
    message: "Failed to resolve response: #{e.message}",
    request: request,
    original_error: e,
  )
end

.success_status?(status) ⇒ Boolean (private)

Returns:

  • (Boolean)


58
59
60
# File 'lib/twelvedata_ruby/response.rb', line 58

def success_status?(status)
  HTTP_STATUSES[:success].include?(status)
end

.valid_status_codesArray<Integer>

Get all valid HTTP status codes

Returns:

  • (Array<Integer>)

    Array of valid status codes



52
53
54
# File 'lib/twelvedata_ruby/response.rb', line 52

def valid_status_codes
  @valid_status_codes ||= HTTP_STATUSES.values.flat_map(&:to_a)
end

Instance Method Details

#attachment_filenameString?

Get attachment filename from response headers

Returns:

  • (String, nil)

    Filename if present in headers



111
112
113
114
115
# File 'lib/twelvedata_ruby/response.rb', line 111

def attachment_filename
  return nil unless headers["content-disposition"]

  @attachment_filename ||= extract_filename_from_headers
end

#content_typeSymbol?

Get content type from response

Returns:

  • (Symbol, nil)

    Content type symbol (:json, :csv, :plain)



120
121
122
# File 'lib/twelvedata_ruby/response.rb', line 120

def content_type
  @content_type ||= detect_content_type
end

#create_response_errorObject (private)



302
303
304
305
306
307
308
309
310
311
312
# File 'lib/twelvedata_ruby/response.rb', line 302

def create_response_error
  error_class_name = ResponseError.error_class_for_code(status_code)
  error_class = error_class_name ? TwelvedataRuby.const_get(error_class_name) : ResponseError

  error_class.new(
    json_data: parsed_body,
    request: request,
    status_code: status_code,
    message: parsed_body[:message],
  )
end

#detect_content_typeObject (private)



217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/twelvedata_ruby/response.rb', line 217

def detect_content_type
  content_type_header = headers["content-type"]
  return :plain unless content_type_header

  case content_type_header
  when /json/
    :json
  when /csv/
    :csv
  else
    :plain
  end
end

#dump_csvObject (private)



288
289
290
291
292
# File 'lib/twelvedata_ruby/response.rb', line 288

def dump_csv
  return nil unless parsed_body.is_a?(CSV::Table)

  parsed_body.to_csv(col_sep: CSV_COL_SEP)
end

#dump_jsonObject (private)



282
283
284
285
286
# File 'lib/twelvedata_ruby/response.rb', line 282

def dump_json
  return nil unless parsed_body.is_a?(Hash)

  JSON.dump(parsed_body)
end

#dump_parsed_bodyString

Get dumped (serialized) version of parsed body

Returns:

  • (String)

    Serialized response body



183
184
185
186
187
188
# File 'lib/twelvedata_ruby/response.rb', line 183

def dump_parsed_body
  handler = CONTENT_TYPE_HANDLERS[content_type]
  return parsed_body.to_s unless handler

  send(handler[:dumper])
end

#errorResponseError?

Get response error if present

Returns:

  • (ResponseError, nil)

    Error object if response contains an error



135
136
137
138
139
# File 'lib/twelvedata_ruby/response.rb', line 135

def error
  return nil unless parsed_body.is_a?(Hash) && parsed_body.key?(:code)

  @error ||= create_response_error
end

#extract_filename_from_headersObject (private)



208
209
210
211
212
213
214
215
# File 'lib/twelvedata_ruby/response.rb', line 208

def extract_filename_from_headers
  disposition = headers["content-disposition"]
  return nil unless disposition

  # Extract filename from Content-Disposition header
  match = disposition.match(/filename="([^"]+)"/)
  match ? match[1] : nil
end

#extract_status_codeObject (private)



294
295
296
297
298
299
300
# File 'lib/twelvedata_ruby/response.rb', line 294

def extract_status_code
  if parsed_body.is_a?(Hash) && parsed_body[:code]
    parsed_body[:code]
  else
    http_status_code
  end
end

#http_status_codeInteger

Get HTTP status code

Returns:

  • (Integer)

    HTTP status code



144
145
146
# File 'lib/twelvedata_ruby/response.rb', line 144

def http_status_code
  http_response.status
end

#inspectString

Detailed inspection of response

Returns:

  • (String)

    Detailed response information



201
202
203
204
# File 'lib/twelvedata_ruby/response.rb', line 201

def inspect
  "#<#{self.class.name}:#{object_id} status=#{http_status_code} content_type=#{content_type} " \
  "size=#{body_bytesize} error=#{error ? "yes" : "no"}>"
end

#parse_body_content(content) ⇒ Object (private)



245
246
247
248
249
250
# File 'lib/twelvedata_ruby/response.rb', line 245

def parse_body_content(content)
  handler = CONTENT_TYPE_HANDLERS[content_type]
  return content unless handler

  send(handler[:parser], content)
end

#parse_csv(content) ⇒ Object (private)



269
270
271
272
273
274
275
276
# File 'lib/twelvedata_ruby/response.rb', line 269

def parse_csv(content)
  CSV.parse(content, headers: true, col_sep: CSV_COL_SEP)
rescue CSV::MalformedCSVError => e
  raise ResponseError.new(
    message: "Failed to parse CSV response: #{e.message}",
    original_error: e,
  )
end

#parse_json(content) ⇒ Object (private)



260
261
262
263
264
265
266
267
# File 'lib/twelvedata_ruby/response.rb', line 260

def parse_json(content)
  JSON.parse(content, symbolize_names: true)
rescue JSON::ParserError => e
  raise ResponseError.new(
    message: "Failed to parse JSON response: #{e.message}",
    original_error: e,
  )
end

#parse_large_body_contentObject (private)



252
253
254
255
256
257
258
# File 'lib/twelvedata_ruby/response.rb', line 252

def parse_large_body_content
  Tempfile.create do |temp_file|
    http_response.body.copy_to(temp_file)
    temp_file.rewind
    parse_body_content(temp_file.read)
  end
end

#parse_plain(content) ⇒ Object (private)



278
279
280
# File 'lib/twelvedata_ruby/response.rb', line 278

def parse_plain(content)
  content.to_s
end

#parse_response_bodyObject (private)



231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/twelvedata_ruby/response.rb', line 231

def parse_response_body
  return nil unless http_response.body

  begin
    if body_bytesize < BODY_MAX_BYTESIZE
      parse_body_content(http_response.body.to_s)
    else
      parse_large_body_content
    end
  ensure
    http_response.body.close if http_response.body.respond_to?(:close)
  end
end

#parsed_bodyHash, ... Also known as: body

Get parsed response body

Returns:

  • (Hash, CSV::Table, String)

    Parsed response data



127
128
129
# File 'lib/twelvedata_ruby/response.rb', line 127

def parsed_body
  @parsed_body ||= parse_response_body
end

#save_to_file(file_path = nil) ⇒ File?

Save response to disk file

Parameters:

  • file_path (String) (defaults to: nil)

    Path to save file (defaults to attachment filename)

Returns:

  • (File, nil)

    File object if successful, nil otherwise



166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/twelvedata_ruby/response.rb', line 166

def save_to_file(file_path = nil)
  file_path ||= attachment_filename
  return nil unless file_path

  File.open(file_path, "w") do |file|
    file.write(dump_parsed_body)
  end
rescue StandardError => e
  raise ResponseError.new(
    message: "Failed to save response to file: #{e.message}",
    original_error: e,
  )
end

#status_codeInteger

Get API status code (from response body)

Returns:

  • (Integer)

    API status code



151
152
153
# File 'lib/twelvedata_ruby/response.rb', line 151

def status_code
  @status_code ||= extract_status_code
end

#success?Boolean

Check if HTTP response was successful

Returns:

  • (Boolean)

    True if HTTP status indicates success



158
159
160
# File 'lib/twelvedata_ruby/response.rb', line 158

def success?
  HTTP_STATUSES[:success].include?(http_status_code)
end

#to_sString

String representation of response

Returns:

  • (String)

    Response summary



193
194
195
196
# File 'lib/twelvedata_ruby/response.rb', line 193

def to_s
  status_info = "#{http_status_code} (#{success? ? "success" : "error"})"
  "Response: #{status_info}, Content-Type: #{content_type}, Size: #{body_bytesize} bytes"
end