# frozen_string_literal: true require "faraday" require "faraday/retry" require "faraday/multipart" module Fiken # Wraps a Faraday connection: bearer auth, JSON encode/decode, retry on # rate-limit/server errors, and mapping of HTTP error statuses to Fiken errors. class Connection DEFAULT_BASE_URL = "https://api.fiken.no/api/v2" Result = Struct.new(:status, :body, :headers, keyword_init: true) DEFAULT_RETRY = { max: 2, interval: 0.5, backoff_factor: 2, retry_statuses: [429, 500, 502, 503, 504], methods: [] # retry regardless of method; Fiken's 429 means "not processed" }.freeze def initialize(token:, base_url: DEFAULT_BASE_URL, user_agent: nil, retry_options: {}, logger: nil, adapter: Faraday.default_adapter) @token = token # Faraday joins relative paths against the base URL, which only behaves # predictably when the base ends in "/" and request paths do not start with one. @base_url = base_url.end_with?("/") ? base_url : "#{base_url}/" @user_agent = user_agent || "fiken-ruby/#{Fiken::VERSION}" @retry_options = DEFAULT_RETRY.merge(retry_options) @logger = logger @adapter = adapter end def get(path, params = nil, headers = nil) run(:get, path, params: params, headers: headers) end def post(path, body = nil, headers = nil) run(:post, path, body: body, headers: headers) end def put(path, body = nil, headers = nil) run(:put, path, body: body, headers: headers) end def patch(path, body = nil, headers = nil) run(:patch, path, body: body, headers: headers) end def delete(path, params = nil, headers = nil) run(:delete, path, params: params, headers: headers) end # Multipart upload (attachments). `parts` is a hash of field name => value, # where a value may be a Faraday::Multipart::FilePart for the file itself. def post_multipart(path, parts) response = multipart_faraday.post(path.sub(%r{\A/}, ""), parts) raise_on_error(response) Result.new(status: response.status, body: response.body, headers: response.headers) rescue Faraday::Error => e raise ConnectionError, e.message end def faraday @faraday ||= Faraday.new(url: @base_url) do |f| f.request :authorization, "Bearer", @token f.request :json f.request :retry, @retry_options f.response :json, content_type: /\bjson$/, parser_options: { symbolize_names: false } f.headers["User-Agent"] = @user_agent f.headers["Accept"] = "application/json" f.response :logger, @logger if @logger f.adapter @adapter end end def multipart_faraday @multipart_faraday ||= Faraday.new(url: @base_url) do |f| f.request :authorization, "Bearer", @token f.request :multipart f.request :url_encoded f.response :json, content_type: /\bjson$/ f.headers["User-Agent"] = @user_agent f.headers["Accept"] = "application/json" f.adapter @adapter end end private def run(method, path, params: nil, body: nil, headers: nil) response = faraday.public_send(method, path.sub(%r{\A/}, "")) do |req| req.params.update(params) if params req.body = body unless body.nil? req.headers.update(headers) if headers end raise_on_error(response) Result.new(status: response.status, body: response.body, headers: response.headers) rescue Faraday::Error => e raise ConnectionError, e.message end def raise_on_error(response) return if response.status < 400 raise Error.from_response(response.status, response.body, response) end end end