Class: Quolle::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/quolle/client.rb

Overview

The Quolle API client.

quolle = Quolle::Client.new(api_key: "qle_...")
result = quolle.emails.send(
from: "hello@mail.yourdomain.com",
to: "customer@example.com",
subject: "Welcome!",
html: "<h1>Thanks for signing up</h1>"
)
puts result["id"]

Constant Summary collapse

DEFAULT_BASE_URL =
"https://api.quolle.com"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(api_key: nil, base_url: DEFAULT_BASE_URL, timeout: 30, max_retries: 3) ⇒ Client

Returns a new instance of Client.

Parameters:

  • api_key (String, nil) (defaults to: nil)

    your API key (falls back to QUOLLE_API_KEY)

  • base_url (String) (defaults to: DEFAULT_BASE_URL)
  • timeout (Integer) (defaults to: 30)

    per-request timeout in seconds

  • max_retries (Integer) (defaults to: 3)

    retries for transient failures (default 3)

Raises:

  • (ArgumentError)


35
36
37
38
39
40
41
42
43
44
# File 'lib/quolle/client.rb', line 35

def initialize(api_key: nil, base_url: DEFAULT_BASE_URL, timeout: 30, max_retries: 3)
  @api_key = api_key || ENV["QUOLLE_API_KEY"]
  raise ArgumentError, "A Quolle API key is required (pass api_key: or set QUOLLE_API_KEY)" if @api_key.nil? || @api_key.empty?

  @base_url = base_url.sub(%r{/+\z}, "")
  @timeout = timeout
  @max_retries = max_retries
  @emails = Resources::Emails.new(self)
  @webhooks = Resources::Webhooks.new(self)
end

Instance Attribute Details

#emailsQuolle::Resources::Emails (readonly)



27
28
29
# File 'lib/quolle/client.rb', line 27

def emails
  @emails
end

#webhooksQuolle::Resources::Webhooks (readonly)



29
30
31
# File 'lib/quolle/client.rb', line 29

def webhooks
  @webhooks
end

Instance Method Details

#request(method, path, body: nil, headers: {}) ⇒ Hash

Perform an authenticated request and return the parsed JSON body.

Transient failures (HTTP 429 and 5xx, plus network errors) are retried with exponential backoff, honoring Retry-After on 429. To avoid double-sending, a POST is only retried on a 5xx/network error when an Idempotency-Key is present; a 429 (never processed) is always safe to retry.

Returns:

  • (Hash)

Raises:

  • (Quolle::Error)

    on a non-2xx response or transport failure after retries



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/quolle/client.rb', line 55

def request(method, path, body: nil, headers: {})
  uri = URI.parse("#{@base_url}#{path}")
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = uri.scheme == "https"
  http.open_timeout = @timeout
  http.read_timeout = @timeout

  klass = {
    "GET" => Net::HTTP::Get,
    "POST" => Net::HTTP::Post,
    "DELETE" => Net::HTTP::Delete
  }.fetch(method.upcase)

  idempotent = %w[GET DELETE].include?(method.upcase)
  has_idem = headers.keys.any? { |k| k.to_s.downcase == "idempotency-key" }

  last_error = nil
  (0..@max_retries).each do |attempt|
    req = klass.new(uri.request_uri)
    req["Authorization"] = "Bearer #{@api_key}"
    req["Content-Type"] = "application/json"
    req["Accept"] = "application/json"
    req["User-Agent"] = "quolle-ruby/#{Quolle::VERSION}"
    headers.each { |k, v| req[k] = v }
    req.body = JSON.generate(body) unless body.nil?

    begin
      resp = http.request(req)
    rescue StandardError => e
      last_error = Quolle::Error.new("Network error: #{e.message}")
      if attempt < @max_retries && (idempotent || has_idem)
        sleep(backoff(attempt, nil))
        next
      end
      raise last_error
    end

    parsed = resp.body.to_s.empty? ? {} : (JSON.parse(resp.body) rescue {})
    status = resp.code.to_i

    if (200..299).cover?(status)
      return parsed
    end

    message = parsed.delete("error") || "Request failed with status #{status}"
    last_error = Quolle::Error.new(message, status_code: status, data: parsed)
    retryable = status == 429 || ((500..504).cover?(status) && (idempotent || has_idem))
    if attempt < @max_retries && retryable
      sleep(backoff(attempt, resp["Retry-After"]))
      next
    end
    raise last_error
  end

  raise(last_error || Quolle::Error.new("Request failed"))
end