Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/lago-ruby-client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
require 'lago/api/client'
require 'lago/api/connection'
require 'lago/api/http_error'
require 'lago/api/retry_limit_error'
109 changes: 74 additions & 35 deletions lib/lago/api/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ module Lago
module Api
class Connection
RESPONSE_SUCCESS_CODES = [200, 201, 202, 204].freeze
RETRY_LIMIT_ERROR_CODE = 429
MAX_RETRIES = 3
RETRY_WAIT_TIME_IN_SECONDS = 1 # FIXME: Add the correct value for the wait time between retries

def initialize(api_key, uri)
@api_key = api_key
Expand All @@ -25,65 +28,79 @@ def post(body, path = uri.path)

def put(path = uri.path, identifier:, body:)
uri_path = identifier.nil? ? path : "#{path}/#{CGI.escapeURIComponent(identifier)}"
response = http_client.send_request(
'PUT',
uri_path,
prepare_payload(body),
headers
)

handle_response(response)
with_retry_limit do
response = http_client.send_request(
'PUT',
uri_path,
prepare_payload(body),
headers
)

handle_response(response)
end
end

def patch(path = uri.path, identifier:, body:)
uri_path = identifier.nil? ? path : "#{path}/#{CGI.escapeURIComponent(identifier)}"
response = http_client.send_request(
'PATCH',
uri_path,
prepare_payload(body),
headers
)

handle_response(response)
with_retry_limit do
response = http_client.send_request(
'PATCH',
uri_path,
prepare_payload(body),
headers
)

handle_response(response)
end
end

def get(path = uri.path, identifier:)
uri_path = identifier.nil? ? path : "#{path}/#{CGI.escapeURIComponent(identifier)}"
response = http_client.send_request(
'GET',
uri_path,
prepare_payload(nil),
headers
)

handle_response(response)
with_retry_limit do
response = http_client.send_request(
'GET',
uri_path,
prepare_payload(nil),
headers
)

handle_response(response)
end
end

def destroy(path = uri.path, identifier:, options: nil)
uri_path = path
uri_path += "/#{CGI.escapeURIComponent(identifier)}" if identifier
uri_path += "?#{URI.encode_www_form(options)}" unless options.nil?
response = http_client.send_request(
'DELETE',
uri_path,
prepare_payload(nil),
headers
)

handle_response(response)
with_retry_limit do
response = http_client.send_request(
'DELETE',
uri_path,
prepare_payload(nil),
headers
)

handle_response(response)
end
end

def get_all(options, path = uri.path)
uri_path = options.empty? ? path : "#{path}?#{URI.encode_www_form(options)}"

response = http_client.send_request(
'GET',
uri_path,
prepare_payload(nil),
headers
)
with_retry_limit do
response = http_client.send_request(
'GET',
uri_path,
prepare_payload(nil),
headers
)

handle_response(response)
handle_response(response)
end
end

private
Expand All @@ -99,6 +116,7 @@ def headers
end

def handle_response(response)
raise_retry_limit_error(response) if response.code.to_i == RETRY_LIMIT_ERROR_CODE
raise_error(response) unless RESPONSE_SUCCESS_CODES.include?(response.code.to_i)

response.body.empty? || JSON.parse(response.body)
Expand All @@ -122,6 +140,27 @@ def prepare_payload(payload)
def raise_error(response)
raise Lago::Api::HttpError.new(response.code.to_i, response.body, uri)
end

def raise_retry_limit_error(response)
raise Lago::Api::RetryLimitError.new(response.code.to_i, response.body, uri)
end

def with_retry_limit
attempts = 0

begin
attempts += 1

yield
rescue Lago::Api::RetryLimitError => e
if attempts < MAX_RETRIES
sleep(RETRY_WAIT_TIME_IN_SECONDS)
retry
end

raise e
end
end
end
end
end
8 changes: 8 additions & 0 deletions lib/lago/api/retry_limit_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

module Lago
module Api
class RetryLimitError < HttpError
end
end
end
144 changes: 131 additions & 13 deletions spec/lago/api/connection_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,57 +12,175 @@

let(:uri) { URI('https://testapi.example.org') }

before do
stub_const('Lago::Api::Connection::RETRY_WAIT_TIME_IN_SECONDS', 0)
end

context 'when an unsuccessful request is made' do
before do
stub_request(:post, 'https://testapi.example.org/NOTFOUND')
.to_return(status: 404, body: "")
.to_return(status: 404, body: '')
end

it 'raises an exception with an integer error code' do
expect { connection.post({}, '/NOTFOUND') }.to raise_error { |exception|
expect(exception).to be_a(Lago::Api::HttpError)
expect(exception.error_code).to eq 404
}
expect { connection.post({}, '/NOTFOUND') }.to(
raise_error do |exception|
expect(exception).to be_a(Lago::Api::HttpError)
expect(exception.error_code).to eq 404
end
)
end
end

describe '#get_all' do
let(:options) { { page: 1, per_page: 10 } }

it do
stub = stub_request(:get, 'https://testapi.example.org/?page=1&per_page=10')

connection.get_all(options, uri)

expect(stub).to have_been_requested
end

context 'when api returns 429' do
before do
stub_request(:get, 'https://testapi.example.org/?page=1&per_page=10')
.to_return(status: 429, body: '').then.to_return(status: 200, body: '{"success":true}')
end

it 'auto retries the request' do
response = connection.get_all(options, uri)
expect(response).to eq({ 'success' => true })
end
end

context 'when api returns 429 (2 times)' do
before do
stub_request(:get, 'https://testapi.example.org/?page=1&per_page=10')
.to_return(status: 429, body: '').times(2).then.to_return(status: 200, body: '{"success":true}')
end

it 'auto retries the request' do
response = connection.get_all(options, uri)
expect(response).to eq({ 'success' => true })
end
end
end

describe '#get' do
let(:identifier) { 'gid://app/Customer/12 34' }

it 'encodes the identifier' do
stub_request(:get, 'https://testapi.example.org:443/gid:%2F%2Fapp%2FCustomer%2F12%2034')

stub = stub_request(:get, 'https://testapi.example.org:443/gid:%2F%2Fapp%2FCustomer%2F12%2034')
connection.get(identifier: identifier)

expect(stub).to have_been_requested
end

context 'when api returns 429' do
before do
stub_request(:get, 'https://testapi.example.org:443/gid:%2F%2Fapp%2FCustomer%2F12%2034')
.to_return(status: 429, body: '').then.to_return(status: 200, body: '{"success":true}')
end

it 'auto retries the request' do
response = connection.get(identifier: identifier)
expect(response).to eq({ 'success' => true })
end
end

context 'when api returns 429 (2 times)' do
before do
stub_request(:get, 'https://testapi.example.org:443/gid:%2F%2Fapp%2FCustomer%2F12%2034')
.to_return(status: 429, body: '').times(2).then.to_return(status: 200, body: '{"success":true}')
end

it 'auto retries the request' do
response = connection.get(identifier: identifier)
expect(response).to eq({ 'success' => true })
end
end
end

describe '#put' do
let(:identifier) { 'gid://app/Customer/12 34' }

it 'encodes the identifier' do
stub_request(:put, 'https://testapi.example.org:443/gid:%2F%2Fapp%2FCustomer%2F12%2034')

stub = stub_request(:put, 'https://testapi.example.org:443/gid:%2F%2Fapp%2FCustomer%2F12%2034')
connection.put(identifier: identifier, body: nil)

expect(stub).to have_been_requested
end

context 'when api returns 429' do
before do
stub_request(:put, 'https://testapi.example.org:443/gid:%2F%2Fapp%2FCustomer%2F12%2034')
.to_return(status: 429, body: nil).then.to_return(status: 200, body: '{"success":true}')
end

it 'auto retries the request' do
response = connection.put(identifier: identifier, body: nil)
expect(response).to eq({ 'success' => true })
end
end

context 'when api returns 429 (2 times)' do
before do
stub_request(:put, 'https://testapi.example.org:443/gid:%2F%2Fapp%2FCustomer%2F12%2034')
.to_return(status: 429, body: nil).times(2).then.to_return(status: 200, body: '{"success":true}')
end

it 'auto retries the request' do
response = connection.put(identifier: identifier, body: nil)
expect(response).to eq({ 'success' => true })
end
end
end

describe '#patch' do
let(:identifier) { 'gid://app/Customer/12 34' }

it 'encodes the identifier' do
stub_request(:patch, 'https://testapi.example.org:443/gid:%2F%2Fapp%2FCustomer%2F12%2034')

stub = stub_request(:patch, 'https://testapi.example.org:443/gid:%2F%2Fapp%2FCustomer%2F12%2034')
connection.patch(identifier: identifier, body: nil)

expect(stub).to have_been_requested
end

context 'when api returns 429' do
before do
stub_request(:patch, 'https://testapi.example.org:443/gid:%2F%2Fapp%2FCustomer%2F12%2034')
.to_return(status: 429, body: nil).then.to_return(status: 200, body: '{"success":true}')
end

it 'auto retries the request' do
response = connection.patch(identifier: identifier, body: nil)
expect(response).to eq({ 'success' => true })
end
end

context 'when api returns 429 (2 times)' do
before do
stub_request(:patch, 'https://testapi.example.org:443/gid:%2F%2Fapp%2FCustomer%2F12%2034')
.to_return(status: 429, body: nil).times(2).then.to_return(status: 200, body: '{"success":true}')
end

it 'auto retries the request' do
response = connection.patch(identifier: identifier, body: nil)
expect(response).to eq({ 'success' => true })
end
end
end

describe '#destroy' do
let(:identifier) { 'gid://app/Customer/12 34' }

it 'encodes the identifier' do
stub_request(:delete, 'https://testapi.example.org:443/gid:%2F%2Fapp%2FCustomer%2F12%2034')

stub = stub_request(:delete, 'https://testapi.example.org:443/gid:%2F%2Fapp%2FCustomer%2F12%2034')
connection.destroy(identifier: identifier)

expect(stub).to have_been_requested
end
end
end
Loading