Class: Inttegro::HTTPClient

Inherits:
Object
  • Object
show all
Extended by:
T::Sig
Defined in:
lib/inttegro/http_client.rb

Overview

Minimal HTTP client that wraps Net::HTTP with JSON parsing and API-friendly errors.

Constant Summary collapse

USER_AGENT =
T.let("inttegro-sdk-ruby/#{Inttegro::VERSION}", String)
ResponseValue =
T.type_alias do
  T.any(T::Struct, ResponseObject, Types::Scalar, T::Array[Object])
end
ErrorPayload =
T.type_alias { T::Hash[Symbol, T.nilable(String)] }

Instance Method Summary collapse

Constructor Details

#initialize(api_key:, base_url: DEFAULT_BASE_URL, read_timeout: 30, open_timeout: 10, adapter: nil, telemetry_enabled: true, tracer_provider: nil, error_reporter: nil, error_reporting_policy: :unexpected) ⇒ HTTPClient

Returns a new instance of HTTPClient.

Raises:

  • (ArgumentError)


45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/inttegro/http_client.rb', line 45

def initialize(
  api_key:,
  base_url: DEFAULT_BASE_URL,
  read_timeout: 30,
  open_timeout: 10,
  adapter: nil,
  telemetry_enabled: true,
  tracer_provider: nil,
  error_reporter: nil,
  error_reporting_policy: :unexpected
)
  @api_key = T.let(api_key || "", String)
  raise ArgumentError, "api_key is required" if @api_key.strip.empty?

  normalized_base_url = T.let(base_url || DEFAULT_BASE_URL, String)
  end_index = T.let(normalized_base_url.length, Integer)
  end_index -= 1 while end_index.positive? && normalized_base_url.getbyte(end_index - 1) == 47
  @base_url = T.let(T.must(normalized_base_url[0, end_index]), String)
  @read_timeout = T.let(read_timeout, T.nilable(Numeric))
  @open_timeout = T.let(open_timeout, T.nilable(Numeric))
  @adapter = T.let(adapter, T.nilable(Types::Adapter))
  @telemetry = T.let(
    Telemetry.new(
      Inttegro::VERSION,
      enabled: telemetry_enabled,
      tracer_provider: tracer_provider,
      error_reporter: error_reporter,
      error_reporting_policy: error_reporting_policy
    ),
    Telemetry
  )
end

Instance Method Details

#get(path, headers: {}, query: nil) ⇒ Object



79
80
81
# File 'lib/inttegro/http_client.rb', line 79

def get(path, headers: {}, query: nil)
  request(:get, path, headers: headers, query: query)
end

#get_binary_public(url) ⇒ Object



304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/inttegro/http_client.rb', line 304

def get_binary_public(url)
  @telemetry.in_span(url, :get, @base_url, Inttegro::VERSION, "file_links.download") do |span|
    uri = build_uri(url, nil)
    request = Net::HTTP::Get.new(uri)
    request["User-Agent"] = USER_AGENT
    @telemetry.prepare(span, request)
    raw_response = @adapter ? @adapter.call(uri, request) : build_http(uri).request(request)
    response = TransportResponse.new(raw_response)
    @telemetry.response(span, response, decoded: false)
    return FileDownload.new(response.body, response.headers) if response.code.to_i < 400

    raise_response_error(response)
  end
end

#perform_multipart(path, fields, files, headers, authenticated, model, operation = nil) ⇒ Object



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/inttegro/http_client.rb', line 251

def perform_multipart(path, fields, files, headers, authenticated, model, operation = nil)
  @telemetry.in_span(path, :post, @base_url, Inttegro::VERSION, operation) do |span|
    uri = build_uri(path, nil)
    request = Net::HTTP::Post.new(uri)
    request["Accept"] = "application/json"
    request["User-Agent"] = USER_AGENT
    request["Authorization"] = "Bearer #{@api_key}" if authenticated
    headers = headers.dup
    if authenticated && idempotent_mutation_path?(path) && !header_present?(headers, "Idempotency-Key")
      headers["Idempotency-Key"] = generate_idempotency_key
    end
    headers.each { |key, value| request[key] = value }

    form = []
    fields.each do |key, value|
      next if value.nil?

      form << [key.to_s, value.is_a?(Hash) || value.is_a?(Array) ? JSON.generate(value) : value.to_s]
    end
    files.each do |key, file_path|
      raise ArgumentError, "file path for #{key} must be a String" unless file_path.is_a?(String)

      form << [key.to_s, File.open(file_path)]
    end
    request.set_form(form, "multipart/form-data")
    @telemetry.prepare(span, request)

    raw_response = @adapter ? @adapter.call(uri, request) : build_http(uri).request(request)
    response = TransportResponse.new(raw_response)
    @telemetry.response(span, response, decoded: false)
    result = handle_response(path, response, model)
    @telemetry.decoded(span)
    result
  end
end

#post(path, body = nil, headers: {}, query: nil, **body_keywords) ⇒ Object



92
93
94
95
# File 'lib/inttegro/http_client.rb', line 92

def post(path, body = nil, headers: {}, query: nil, **body_keywords)
  body = body_keywords unless body_keywords.empty?
  request(:post, path, body: body, headers: headers, query: query)
end

#post_binary_json(path, body) ⇒ Object



288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/inttegro/http_client.rb', line 288

def post_binary_json(path, body)
  @telemetry.in_span(path, :post, @base_url, Inttegro::VERSION) do |span|
    payload = with_request_meta_idempotency(:post, path, coerce_body(body), {})
    response = raw_request(:post, path, JSON.dump(payload), {
      "Accept" => "application/octet-stream",
      "Content-Type" => "application/json",
      "Authorization" => "Bearer #{@api_key}"
    }, span)
    @telemetry.response(span, response, decoded: false)
    return FileDownload.new(response.body, response.headers) if response.code.to_i < 400

    raise_response_error(response)
  end
end

#post_model(path, model, body, headers: {}, query: nil) ⇒ Object

Raises:

  • (TypeError)


108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/inttegro/http_client.rb', line 108

def post_model(path, model, body, headers: {}, query: nil)
  unless model <= T::Struct
    raise TypeError, "response model for #{path} must inherit from T::Struct"
  end

  response = request(
    :post,
    path,
    body: body,
    headers: headers,
    query: query,
    response_model: model
  )
  return T.cast(response, T.type_parameter(:Model)) if response.is_a?(model)

  raise TypeError, "expected #{model} from #{path}, got #{response.class}"
end

#post_multipart(path, fields: {}, files: {}, headers: {}, authenticated: true) ⇒ Object



178
179
180
181
182
183
# File 'lib/inttegro/http_client.rb', line 178

def post_multipart(path, fields: {}, files: {}, headers: {}, authenticated: true)
  expect_response_object(
    perform_multipart(path, fields, files, headers, authenticated, nil),
    path
  )
end

#post_multipart_model(path, model, fields: {}, files: {}, headers: {}, authenticated: true, operation: nil) ⇒ Object

Raises:

  • (TypeError)


198
199
200
201
202
203
204
205
206
207
# File 'lib/inttegro/http_client.rb', line 198

def post_multipart_model(path, model, fields: {}, files: {}, headers: {}, authenticated: true, operation: nil)
  unless model <= T::Struct
    raise TypeError, "response model for #{path} must inherit from T::Struct"
  end

  response = perform_multipart(path, fields, files, headers, authenticated, model, operation)
  return T.cast(response, T.type_parameter(:Model)) if response.is_a?(model)

  raise TypeError, "expected #{model} from #{path}, got #{response.class}"
end

#post_multipart_resource(path, model, field, fields: {}, files: {}, headers: {}, authenticated: true) ⇒ Object

Raises:

  • (TypeError)


222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/inttegro/http_client.rb', line 222

def post_multipart_resource(path, model, field, fields: {}, files: {}, headers: {}, authenticated: true)
  envelope = expect_response_object(perform_multipart(
    path,
    fields,
    files,
    headers,
    authenticated,
    nil
  ), path)
  value = envelope.public_send(field)
  return value if value.is_a?(model)

  decoded = Inttegro.deserialize(value, T.cast(model, T::Class[T::Struct]))
  return decoded if decoded.is_a?(model)

  raise TypeError, "expected #{model} in #{field} from #{path}, got #{decoded.class}"
end

#post_object(path, body, headers: {}, query: nil) ⇒ Object



158
159
160
# File 'lib/inttegro/http_client.rb', line 158

def post_object(path, body, headers: {}, query: nil)
  expect_response_object(request(:post, path, body: body, headers: headers, query: query), path)
end

#post_resource(path, model, field, body, headers: {}, query: nil) ⇒ Object

Raises:

  • (TypeError)


138
139
140
141
142
143
144
145
146
147
148
# File 'lib/inttegro/http_client.rb', line 138

def post_resource(path, model, field, body, headers: {}, query: nil)
  envelope = post_object(path, body, headers: headers, query: query)
  value = envelope.public_send(field)
  value = value.to_h if value.is_a?(ResponseObject)
  return value if value.is_a?(model)

  decoded = Inttegro.deserialize(value, T.cast(model, T::Class[T::Struct]))
  return decoded if decoded.is_a?(model)

  raise TypeError, "expected #{model} in #{field} from #{path}, got #{decoded.class}"
end

#post_with_headers(path, body, headers: {}) ⇒ Object



165
166
167
# File 'lib/inttegro/http_client.rb', line 165

def post_with_headers(path, body, headers: {})
  post_object(path, body, headers: headers)
end

#request(method, path, body: nil, headers: {}, query: nil, response_model: nil) ⇒ Object



329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# File 'lib/inttegro/http_client.rb', line 329

def request(method, path, body: nil, headers: {}, query: nil, response_model: nil)
  @telemetry.in_span(path, method, @base_url, Inttegro::VERSION) do |span|
    uri = build_uri(path, query)
    body = validate_body(path, coerce_body(body))
    body = with_request_meta_idempotency(method, path, body, headers)
    request = build_request(method, uri, body, headers)
    @telemetry.prepare(span, request)

    raw_response =
      if @adapter
        @adapter.call(uri, request)
      else
        http = build_http(uri)
        http.request(request)
      end
    response = TransportResponse.new(raw_response)
    @telemetry.response(span, response, decoded: false)
    result = handle_response(path, response, response_model)
    @telemetry.decoded(span)
    result
  end
rescue Timeout::Error, Errno::ETIMEDOUT => e
  raise TimeoutError.new("Request timed out", e)
rescue SocketError, Errno::ECONNREFUSED, Errno::ECONNRESET => e
  raise NetworkError.new("Network request failed", e)
end