Class: Tina4::API
- Inherits:
-
Object
- Object
- Tina4::API
- Defined in:
- lib/tina4/api.rb
Instance Attribute Summary collapse
-
#base_url ⇒ Object
readonly
Returns the value of attribute base_url.
-
#headers ⇒ Object
readonly
Returns the value of attribute headers.
Instance Method Summary collapse
- #add_headers(headers) ⇒ Object
- #delete(path, body: nil) ⇒ Object
-
#download(path, dest_path: nil, params: {}) ⇒ Object
Stream a GET response body to dest_path in chunks.
- #get(path, params: {}) ⇒ Object
-
#initialize(base_url, headers: {}, timeout: 30, bearer_token: nil, username: nil, password: nil, verify_ssl: nil, max_retries: 0, retry_backoff: 0.5, transport: nil, cookies: false) ⇒ API
constructor
3.13.1: added ergonomic kwargs (bearer_token, username, password, verify_ssl) so callers no longer need three follow-up setter calls.
- #patch(path, body: nil, content_type: "application/json") ⇒ Object
- #post(path, body: nil, content_type: "application/json") ⇒ Object
- #put(path, body: nil, content_type: "application/json") ⇒ Object
- #send_request(method = "GET", path = "", body: nil, content_type: "application/json") ⇒ Object
- #set_basic_auth(username, password) ⇒ Object
- #set_bearer_token(token) ⇒ Object
-
#upload(path, file_path: nil, field_name: "file", extra_fields: {}, headers: {}, file_bytes: nil, filename: nil) ⇒ Object
POST a multipart/form-data body — a file plus optional text fields.
Constructor Details
#initialize(base_url, headers: {}, timeout: 30, bearer_token: nil, username: nil, password: nil, verify_ssl: nil, max_retries: 0, retry_backoff: 0.5, transport: nil, cookies: false) ⇒ API
3.13.1: added ergonomic kwargs (bearer_token, username, password, verify_ssl) so callers no longer need three follow-up setter calls. Cross-framework parity with the Python tina4_python.api.Api kwargs.
api = Tina4::API.new("https://api.example.com", bearer_token: "sk-abc")
api = Tina4::API.new("https://api.example.com", username: "u", password: "p")
api = Tina4::API.new("https://api.example.com", headers: {"X-Tenant" => "acme"})
api = Tina4::API.new("https://self-signed.local", verify_ssl: false)
Bearer wins over basic-auth when both are passed.
3.13.39: +max_retries / +retry_backoff enable opt-in automatic retry with exponential backoff (default max_retries: 0 = off, non-breaking) on a transport error (APIResponse#status == 0) or a retryable status (429/5xx). A retried non-idempotent request (POST/PUT/PATCH/DELETE) may be re-sent — retries are opt-in for exactly that reason. Parity with the Python master.
3.13.69: +transport (an injectable seam) and +cookies (an opt-in per-client cookie jar), both parity with the Python master.
transport (default nil = the real Net::HTTP network path) is an injectable
seam so that APPLICATION developers can unit-test their own code without a
live server. When supplied it must respond to #call with the signature
call(method, url, headers, body, timeout) and return a Hash shaped like
{http_code:/status:, body:, headers:, error:} (string or symbol keys). It
fully REPLACES the network call. NOTE: Tina4's own suite must NEVER inject a
fake/canned transport — the no-mock rule stands, so framework tests always
exercise the real network path against a real local server. The seam exists
purely for application-developer testing.
cookies (default false = off, zero behaviour change) turns on a
per-client, in-memory cookie jar: Set-Cookie headers on responses are
parsed (leading name=value only, last write wins) and the accumulated
Cookie header is sent on subsequent requests. Not persisted; scoped to
this instance.
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
# File 'lib/tina4/api.rb', line 101 def initialize(base_url, headers: {}, timeout: 30, bearer_token: nil, username: nil, password: nil, verify_ssl: nil, max_retries: 0, retry_backoff: 0.5, transport: nil, cookies: false) if !transport.nil? && !transport.respond_to?(:call) raise ArgumentError, "transport must respond to #call(method, url, headers, body, timeout)" end @base_url = base_url.chomp("/") @headers = { "Content-Type" => "application/json", "Accept" => "application/json" }.merge(headers) @timeout = timeout @verify_ssl = verify_ssl @max_retries = [0, max_retries.to_i].max @retry_backoff = retry_backoff.to_f @transport = transport @cookies_enabled = ? true : false @cookies = {} # Bearer wins over basic-auth when both passed if bearer_token set_bearer_token(bearer_token) elsif username && password set_basic_auth(username, password) end end |
Instance Attribute Details
#base_url ⇒ Object (readonly)
Returns the value of attribute base_url.
63 64 65 |
# File 'lib/tina4/api.rb', line 63 def base_url @base_url end |
#headers ⇒ Object (readonly)
Returns the value of attribute headers.
63 64 65 |
# File 'lib/tina4/api.rb', line 63 def headers @headers end |
Instance Method Details
#add_headers(headers) ⇒ Object
284 285 286 287 |
# File 'lib/tina4/api.rb', line 284 def add_headers(headers) @headers.merge!(headers) self end |
#delete(path, body: nil) ⇒ Object
170 171 172 173 174 175 176 |
# File 'lib/tina4/api.rb', line 170 def delete(path, body: nil) uri = build_uri(path) request = Net::HTTP::Delete.new(uri) request.body = body.is_a?(String) ? body : JSON.generate(body) if body apply_headers(request, {}) execute(uri, request) end |
#download(path, dest_path: nil, params: {}) ⇒ Object
Stream a GET response body to dest_path in chunks.
The body is written to disk API_DOWNLOAD_CHUNK_SIZE bytes at a time instead of being buffered whole in memory — safe for large payloads. Follows redirects (with the same cross-origin auth/cookie strip as every verb) and honours verify_ssl.
Returns an APIResponse carrying path (and no body — it went to disk).
path is dest_path on success and nil on any error (missing dest, HTTP
error status, or a transport failure); the destination file is not written
on a pre-flight or HTTP-status error. status is 0 on a transport failure.
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 |
# File 'lib/tina4/api.rb', line 249 def download(path, dest_path: nil, params: {}) return error_response("download requires dest_path", path: nil) unless dest_path uri = build_uri(path, params) headers = @headers.dup = headers["Cookie"] = if @cookies_enabled && return download_via_transport(uri, headers, dest_path) if @transport begin result = network_call("GET", uri, headers, nil, stream_to: dest_path) code = result[:status] if result[:path] APIResponse.new(status: code, body: nil, headers: result[:headers], error: nil, path: result[:path]) else APIResponse.new(status: code, body: nil, headers: result[:headers], error: "download failed (HTTP #{code})", path: nil) end rescue StandardError => e APIResponse.new(status: 0, body: nil, headers: {}, error: e., path: nil) end end |
#get(path, params: {}) ⇒ Object
130 131 132 133 134 135 |
# File 'lib/tina4/api.rb', line 130 def get(path, params: {}) uri = build_uri(path, params) request = Net::HTTP::Get.new(uri) apply_headers(request, {}) execute(uri, request) end |
#patch(path, body: nil, content_type: "application/json") ⇒ Object
159 160 161 162 163 164 165 166 167 168 |
# File 'lib/tina4/api.rb', line 159 def patch(path, body: nil, content_type: "application/json") uri = build_uri(path) request = Net::HTTP::Patch.new(uri) if body request.body = body.is_a?(String) ? body : JSON.generate(body) request["Content-Type"] = content_type end apply_headers(request, {}) execute(uri, request) end |
#post(path, body: nil, content_type: "application/json") ⇒ Object
137 138 139 140 141 142 143 144 145 146 |
# File 'lib/tina4/api.rb', line 137 def post(path, body: nil, content_type: "application/json") uri = build_uri(path) request = Net::HTTP::Post.new(uri) if body request.body = body.is_a?(String) ? body : JSON.generate(body) request["Content-Type"] = content_type end apply_headers(request, {}) execute(uri, request) end |
#put(path, body: nil, content_type: "application/json") ⇒ Object
148 149 150 151 152 153 154 155 156 157 |
# File 'lib/tina4/api.rb', line 148 def put(path, body: nil, content_type: "application/json") uri = build_uri(path) request = Net::HTTP::Put.new(uri) if body request.body = body.is_a?(String) ? body : JSON.generate(body) request["Content-Type"] = content_type end apply_headers(request, {}) execute(uri, request) end |
#send_request(method = "GET", path = "", body: nil, content_type: "application/json") ⇒ Object
289 290 291 292 293 294 295 296 297 298 |
# File 'lib/tina4/api.rb', line 289 def send_request(method = "GET", path = "", body: nil, content_type: "application/json") case method.upcase when "GET" then get(path) when "POST" then post(path, body: body, content_type: content_type) when "PUT" then put(path, body: body, content_type: content_type) when "PATCH" then patch(path, body: body, content_type: content_type) when "DELETE" then delete(path, body: body) else get(path) end end |
#set_basic_auth(username, password) ⇒ Object
274 275 276 277 |
# File 'lib/tina4/api.rb', line 274 def set_basic_auth(username, password) @headers["Authorization"] = "Basic #{Base64.strict_encode64("#{username}:#{password}")}" self end |
#set_bearer_token(token) ⇒ Object
279 280 281 282 |
# File 'lib/tina4/api.rb', line 279 def set_bearer_token(token) @headers["Authorization"] = "Bearer #{token}" self end |
#upload(path, file_path: nil, field_name: "file", extra_fields: {}, headers: {}, file_bytes: nil, filename: nil) ⇒ Object
POST a multipart/form-data body — a file plus optional text fields.
BREAKING (3.13.69): reconciled to the canonical cross-framework shape.
file_path was a REQUIRED positional; it is now a keyword. Two ways to
supply the file, so a caller never needs a temp file:
- file_path: — a file on disk. filename: defaults to its basename.
- file_bytes: + filename: — an in-memory payload (String bytes).
field_name: is the form field the file is sent under (default "file"). extra_fields: (Hash) become additional text parts. headers: (Hash) are extra per-call headers merged onto the request. The part's Content-Type is guessed from the filename (falling back to application/octet-stream) — this replaces the old hard-coded application/octet-stream. The client's default headers (including any Authorization) are sent too, unlike the old upload().
Returns an APIResponse. A missing file or no source given returns a clean error response (status 0, error set) — it does NOT raise, and nothing is sent over the wire.
api.upload("/avatars", file_path: "/tmp/me.png")
api.upload("/avatars", file_bytes: raw, filename: "me.png",
extra_fields: { "user_id" => "42" })
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'lib/tina4/api.rb', line 201 def upload(path, file_path: nil, field_name: "file", extra_fields: {}, headers: {}, file_bytes: nil, filename: nil) unless file_bytes.nil? content = file_bytes.is_a?(String) ? file_bytes : file_bytes.to_s upload_name = filename || "upload.bin" end if content.nil? && file_path return error_response("file not found: #{file_path}") unless File.file?(file_path) begin content = File.binread(file_path) rescue SystemCallError => e return error_response(e.) end upload_name = filename || File.basename(file_path) end return error_response("upload requires file_path or file_bytes") if content.nil? part_content_type = guess_content_type(upload_name) boundary = "----Tina4Boundary#{SecureRandom.hex(16)}" body = build_multipart_body(boundary, field_name, upload_name, content, part_content_type, extra_fields) uri = build_uri(path) request = Net::HTTP::Post.new(uri) request.body = body # Apply the client default headers (auth, etc.) FIRST, then force the # multipart Content-Type so it wins over the default application/json, # then any per-call headers last (parity with the Python master's order). apply_headers(request, {}) request["Content-Type"] = "multipart/form-data; boundary=#{boundary}" headers.each { |key, value| request[key] = value } execute(uri, request) end |