Class: SeatLayer::HTTPClient

Inherits:
Object
  • Object
show all
Defined in:
lib/seatlayer/http_client.rb

Overview

The transport: auth, idempotency, retry, and error mapping.

Built on net/http from the standard library rather than Faraday or HTTParty. A server SDK that drags in an HTTP stack becomes a supply-chain surface for every customer who installs it, and can conflict with whatever the host application already uses.

Constant Summary collapse

DEFAULT_BASE_URL =
"https://api.seatlayer.io"
DEFAULT_MAX_RETRIES =
3
DEFAULT_TIMEOUT =
30.0
IDEMPOTENCY_KEY_PATTERN =

The API's own charset for Idempotency-Key.

/\A[A-Za-z0-9._:-]{1,128}\z/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(secret_key:, base_url: DEFAULT_BASE_URL, max_retries: DEFAULT_MAX_RETRIES, timeout: DEFAULT_TIMEOUT, transport: nil) ⇒ HTTPClient

Returns a new instance of HTTPClient.

Raises:

  • (ArgumentError)


27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/seatlayer/http_client.rb', line 27

def initialize(secret_key:, base_url: DEFAULT_BASE_URL, max_retries: DEFAULT_MAX_RETRIES,
               timeout: DEFAULT_TIMEOUT, transport: nil)
  raise ArgumentError, "A SeatLayer secret key is required." if secret_key.nil? || secret_key.empty?

  # Caught here rather than as a 401 three round-trips later. The pk_ case
  # gets its own message: it is the one people paste by mistake.
  if secret_key.start_with?("pk_")
    raise ArgumentError,
          "That is a publishable key. The server SDK needs a secret key (sk_live_… or sk_test_…)."
  end
  unless secret_key.start_with?("sk_")
    raise ArgumentError, "A SeatLayer secret key starts with sk_live_ or sk_test_."
  end

  @secret_key = secret_key
  @base_url = base_url.sub(%r{/+\z}, "")
  @max_retries = max_retries
  @timeout = timeout
  @transport = transport
  @mode = if secret_key.start_with?("sk_test_") then "test"
          elsif secret_key.start_with?("sk_live_") then "live"
          else "unknown"
          end
end

Instance Attribute Details

#base_urlObject (readonly)

Returns the value of attribute base_url.



25
26
27
# File 'lib/seatlayer/http_client.rb', line 25

def base_url
  @base_url
end

#modeString (readonly)

Returns "live", "test", or "unknown", derived from the key prefix.

Returns:

  • (String)

    "live", "test", or "unknown", derived from the key prefix.



24
25
26
# File 'lib/seatlayer/http_client.rb', line 24

def mode
  @mode
end

Class Method Details

.encode(segment) ⇒ Object

Percent-encode a path segment, including slashes.



61
62
63
# File 'lib/seatlayer/http_client.rb', line 61

def self.encode(segment)
  URI.encode_www_form_component(segment.to_s).gsub("+", "%20")
end

.validate_idempotency_key!(key) ⇒ Object

Raises:

  • (ArgumentError)


52
53
54
55
56
57
58
# File 'lib/seatlayer/http_client.rb', line 52

def self.validate_idempotency_key!(key)
  return if IDEMPOTENCY_KEY_PATTERN.match?(key)

  raise ArgumentError,
        "Invalid Idempotency-Key #{key.inspect}: allowed characters are " \
        "A-Z a-z 0-9 . _ : - and the length must be 1-128."
end

Instance Method Details

#delete(path, query = nil) ⇒ Object



116
117
118
# File 'lib/seatlayer/http_client.rb', line 116

def delete(path, query = nil)
  request("DELETE", path, query: query)
end

#get(path, query = nil) ⇒ Object



96
97
98
# File 'lib/seatlayer/http_client.rb', line 96

def get(path, query = nil)
  request("GET", path, query: query)
end

#patch(path, body) ⇒ Object



112
113
114
# File 'lib/seatlayer/http_client.rb', line 112

def patch(path, body)
  request("PATCH", path, body: body)
end

#post(path, body = nil, idempotency_key: nil, retry_policy: :none) ⇒ Object



100
101
102
# File 'lib/seatlayer/http_client.rb', line 100

def post(path, body = nil, idempotency_key: nil, retry_policy: :none)
  request("POST", path, body: body, idempotency_key: idempotency_key, retry_policy: retry_policy)
end

#put(path, body) ⇒ Object



104
105
106
# File 'lib/seatlayer/http_client.rb', line 104

def put(path, body)
  request("PUT", path, body: body)
end

#put_raw(path, raw_body, content_type: "application/octet-stream") ⇒ Object



108
109
110
# File 'lib/seatlayer/http_client.rb', line 108

def put_raw(path, raw_body, content_type: "application/octet-stream")
  request("PUT", path, raw_body: raw_body, content_type: content_type)
end

#request(method, path, query: nil, body: nil, raw_body: nil, content_type: nil, idempotency_key: nil, retry_policy: :none) ⇒ Object



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
# File 'lib/seatlayer/http_client.rb', line 65

def request(method, path, query: nil, body: nil, raw_body: nil, content_type: nil,
            idempotency_key: nil, retry_policy: :none)
  url = @base_url + path
  if query
    pairs = query.compact
    url += "?#{URI.encode_www_form(pairs)}" unless pairs.empty?
  end

  headers = {
    "Authorization" => "Bearer #{@secret_key}",
    "Accept" => "application/json",
    "User-Agent" => "seatlayer-ruby"
  }
  payload = build_payload(body, raw_body, content_type, headers)

  # Only operations with exact server-side response replay get an automatic
  # key. A caller key on any other mutation is forwarded, but cannot opt that
  # operation into automatic retries.
  unless %w[GET HEAD].include?(method)
    key = idempotency_key
    key ||= SecureRandom.uuid if retry_policy == :header_replay
    if key
      self.class.validate_idempotency_key!(key)
      headers["Idempotency-Key"] = key
    end
  end

  retry_allowed = %w[GET HEAD].include?(method) || retry_policy == :header_replay
  execute(method, url, headers, payload, retry_allowed: retry_allowed)
end