Skip to content

Commit 0344c0b

Browse files
authored
Merge pull request #38 from tharropoulos/faraday
Breaking: Migrate HTTP Client from Typhoeus to Faraday and Add Integration Tests
2 parents aa9bed2 + f3e97ff commit 0344c0b

File tree

5 files changed

+157
-28
lines changed

5 files changed

+157
-28
lines changed

.github/workflows/tests.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,25 @@ jobs:
1111
strategy:
1212
matrix:
1313
ruby-version: ['2.7', '3.0', '3.2']
14+
services:
15+
typesense:
16+
image: typesense/typesense:27.1
17+
ports:
18+
- 8108:8108
19+
volumes:
20+
- /tmp/typesense-data:/data
21+
- /tmp/typesense-analytics:/analytics
22+
env:
23+
TYPESENSE_API_KEY: xyz
24+
TYPESENSE_DATA_DIR: /data
25+
TYPESENSE_ENABLE_CORS: true
26+
TYPESENSE_ANALYTICS_DIR: /analytics
27+
TYPESENSE_ENABLE_SEARCH_ANALYTICS: true
1428

1529
steps:
30+
- name: Wait for Typesense
31+
run: |
32+
timeout 20 bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:8108/health)" != "200" ]]; do sleep 1; done' || false
1633
- uses: actions/checkout@v3
1734
- uses: ruby/setup-ruby@v1
1835
with:

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
88
gem 'awesome_print', '~> 1.8'
99
gem 'bundler', '~> 2.0'
1010
gem 'codecov', '~> 0.1'
11+
gem 'erb'
1112
gem 'guard', '~> 2.16'
1213
gem 'guard-rubocop', '~> 1.3'
1314
gem 'rake', '~> 13.0'

lib/typesense/api_call.rb

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen_string_literal: true
22

3-
require 'typhoeus'
3+
require 'faraday'
44
require 'oj'
55

66
module Typesense
@@ -69,23 +69,25 @@ def perform_request(method, endpoint, query_parameters: nil, body_parameters: ni
6969
@logger.debug "Attempting #{method.to_s.upcase} request Try ##{num_tries} to Node #{node[:index]}"
7070

7171
begin
72-
request_options = {
73-
method: method,
74-
timeout: @connection_timeout_seconds,
75-
headers: default_headers.merge(additional_headers)
76-
}
77-
request_options.merge!(params: query_parameters) unless query_parameters.nil?
78-
79-
unless body_parameters.nil?
80-
body = body_parameters
81-
body = Oj.dump(body_parameters, mode: :compat) if request_options[:headers]['Content-Type'] == 'application/json'
82-
request_options.merge!(body: body)
72+
conn = Faraday.new(uri_for(endpoint, node)) do |f|
73+
f.options.timeout = @connection_timeout_seconds
74+
f.options.open_timeout = @connection_timeout_seconds
8375
end
8476

85-
response = Typhoeus::Request.new(uri_for(endpoint, node), request_options).run
86-
set_node_healthcheck(node, is_healthy: true) if response.code >= 1 && response.code <= 499
77+
headers = default_headers.merge(additional_headers)
8778

88-
@logger.debug "Request #{method}:#{uri_for(endpoint, node)} to Node #{node[:index]} was successfully made (at the network layer). Response Code was #{response.code}."
79+
response = conn.send(method) do |req|
80+
req.headers = headers
81+
req.params = query_parameters unless query_parameters.nil?
82+
unless body_parameters.nil?
83+
body = body_parameters
84+
body = Oj.dump(body_parameters, mode: :compat) if headers['Content-Type'] == 'application/json'
85+
req.body = body
86+
end
87+
end
88+
set_node_healthcheck(node, is_healthy: true) if response.status >= 1 && response.status <= 499
89+
90+
@logger.debug "Request #{method}:#{uri_for(endpoint, node)} to Node #{node[:index]} was successfully made (at the network layer). response.status was #{response.status}."
8991

9092
parsed_response = if response.headers && (response.headers['content-type'] || '').include?('application/json')
9193
Oj.load(response.body, mode: :compat)
@@ -94,13 +96,15 @@ def perform_request(method, endpoint, query_parameters: nil, body_parameters: ni
9496
end
9597

9698
# If response is 2xx return the object, else raise the response as an exception
97-
return parsed_response if response.code >= 200 && response.code <= 299
99+
return parsed_response if response.status >= 200 && response.status <= 299
98100

99101
exception_message = (parsed_response && parsed_response['message']) || 'Error'
100102
raise custom_exception_klass_for(response), exception_message
101-
rescue Errno::EINVAL, Errno::ENETDOWN, Errno::ENETUNREACH, Errno::ENETRESET, Errno::ECONNABORTED, Errno::ECONNRESET,
102-
Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::EHOSTDOWN, Errno::EHOSTUNREACH,
103-
Typesense::Error::TimeoutError, Typesense::Error::ServerError, Typesense::Error::HTTPStatus0Error => e
103+
rescue Faraday::ConnectionFailed, Faraday::TimeoutError,
104+
Errno::EINVAL, Errno::ENETDOWN, Errno::ENETUNREACH, Errno::ENETRESET,
105+
Errno::ECONNABORTED, Errno::ECONNRESET, Errno::ETIMEDOUT,
106+
Errno::ECONNREFUSED, Errno::EHOSTDOWN, Errno::EHOSTUNREACH,
107+
Typesense::Error::ServerError, Typesense::Error::HTTPStatus0Error => e
104108
# Rescue network layer exceptions and HTTP 5xx errors, so the loop can continue.
105109
# Using loops for retries instead of rescue...retry to maintain consistency with client libraries in
106110
# other languages that might not support the same construct.
@@ -176,23 +180,24 @@ def set_node_healthcheck(node, is_healthy:)
176180
end
177181

178182
def custom_exception_klass_for(response)
179-
if response.code == 400
183+
if response.status == 400
180184
Typesense::Error::RequestMalformed.new(response: response)
181-
elsif response.code == 401
185+
elsif response.status == 401
182186
Typesense::Error::RequestUnauthorized.new(response: response)
183-
elsif response.code == 404
187+
elsif response.status == 404
184188
Typesense::Error::ObjectNotFound.new(response: response)
185-
elsif response.code == 409
189+
elsif response.status == 409
186190
Typesense::Error::ObjectAlreadyExists.new(response: response)
187-
elsif response.code == 422
191+
elsif response.status == 422
188192
Typesense::Error::ObjectUnprocessable.new(response: response)
189-
elsif response.code >= 500 && response.code <= 599
193+
elsif response.status >= 500 && response.status <= 599
190194
Typesense::Error::ServerError.new(response: response)
191-
elsif response.timed_out?
195+
elsif response.respond_to?(:timed_out?) && response.timed_out?
192196
Typesense::Error::TimeoutError.new(response: response)
193-
elsif response.code.zero?
197+
elsif response.status.zero?
194198
Typesense::Error::HTTPStatus0Error.new(response: response)
195199
else
200+
# This will handle both 300-level responses and any other unhandled status codes
196201
Typesense::Error::HTTPError.new(response: response)
197202
end
198203
end

spec/typesense/collections_spec.rb

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,112 @@
5050

5151
expect(result).to eq(company_schema)
5252
end
53+
54+
context 'with integration', :integration do
55+
let(:integration_schema) do
56+
{
57+
'name' => 'integration_companies',
58+
'fields' => [
59+
{
60+
'name' => 'company_name',
61+
'type' => 'string',
62+
'facet' => false
63+
},
64+
{
65+
'name' => 'num_employees',
66+
'type' => 'int32',
67+
'facet' => false
68+
},
69+
{
70+
'name' => 'country',
71+
'type' => 'string',
72+
'facet' => true
73+
}
74+
],
75+
'default_sorting_field' => 'num_employees'
76+
}
77+
end
78+
79+
let(:integration_client) do
80+
Typesense::Client.new(
81+
nodes: [{
82+
host: 'localhost',
83+
port: '8108',
84+
protocol: 'http'
85+
}],
86+
api_key: 'xyz',
87+
connection_timeout_seconds: 10
88+
)
89+
end
90+
91+
let(:expected_fields) do
92+
[
93+
{
94+
'name' => 'company_name',
95+
'type' => 'string',
96+
'facet' => false,
97+
'index' => true,
98+
'infix' => false,
99+
'locale' => '',
100+
'optional' => false,
101+
'sort' => false,
102+
'stem' => false,
103+
'store' => true
104+
},
105+
{
106+
'name' => 'num_employees',
107+
'type' => 'int32',
108+
'facet' => false,
109+
'index' => true,
110+
'infix' => false,
111+
'locale' => '',
112+
'optional' => false,
113+
'sort' => true,
114+
'stem' => false,
115+
'store' => true
116+
},
117+
{
118+
'name' => 'country',
119+
'type' => 'string',
120+
'facet' => true,
121+
'index' => true,
122+
'infix' => false,
123+
'locale' => '',
124+
'optional' => false,
125+
'sort' => false,
126+
'stem' => false,
127+
'store' => true
128+
}
129+
]
130+
end
131+
132+
before do
133+
WebMock.disable!
134+
begin
135+
integration_client.collections['integration_companies'].delete
136+
rescue Typesense::Error::ObjectNotFound
137+
# Collection doesn't exist, which is fine
138+
end
139+
end
140+
141+
after do
142+
begin
143+
integration_client.collections['integration_companies'].delete
144+
rescue Typesense::Error::ObjectNotFound
145+
# Collection doesn't exist, which is fine
146+
end
147+
WebMock.enable!
148+
end
149+
150+
it 'creates a collection on a real Typesense server' do
151+
result = integration_client.collections.create(integration_schema)
152+
153+
expect(result['name']).to eq('integration_companies')
154+
expect(result['fields']).to eq(expected_fields)
155+
expect(result['default_sorting_field']).to eq(integration_schema['default_sorting_field'])
156+
expect(result['num_documents']).to eq(0)
157+
end
158+
end
53159
end
54160

55161
describe '#retrieve' do

typesense.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
2626
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
2727
spec.require_paths = ['lib']
2828

29+
spec.add_dependency 'faraday', '~> 2.8'
2930
spec.add_dependency 'oj', '~> 3.16'
30-
spec.add_dependency 'typhoeus', '~> 1.4'
3131
spec.metadata['rubygems_mfa_required'] = 'true'
3232
end

0 commit comments

Comments
 (0)