Skip to content

Commit b9d6627

Browse files
committed
Switch http client to typhoeus (libcurl wrapper), fix linting issues, added guard
1 parent 530a811 commit b9d6627

24 files changed

+137
-144
lines changed

.rubocop.yml

+21-16
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,39 @@
1-
inherit_from: .rubocop_todo.yml
2-
3-
require: rubocop-rspec
4-
51
AllCops:
6-
TargetRubyVersion: "2.4"
72
NewCops: enable
83

9-
Metrics/BlockLength:
10-
Exclude:
11-
- 'spec/**/*'
12-
- 'Gemfile'
13-
- 'typesense.gemspec'
14-
154
Style/Documentation:
165
Enabled: false
176

18-
RSpec/ExampleLength:
7+
Metrics/AbcSize:
198
Enabled: false
209

21-
Metrics/MethodLength:
10+
Metrics/CyclomaticComplexity:
2211
Enabled: false
2312

24-
Metrics/AbcSize:
13+
Metrics/PerceivedComplexity:
2514
Enabled: false
2615

2716
Metrics/ClassLength:
2817
Enabled: false
2918

30-
Metrics/CyclomaticComplexity:
19+
Metrics/ParameterLists:
3120
Enabled: false
3221

33-
Metrics/PerceivedComplexity:
22+
Metrics/MethodLength:
23+
Enabled: false
24+
25+
Metrics/BlockLength:
3426
Enabled: false
27+
28+
Metrics/BlockNesting:
29+
Enabled: false
30+
31+
Style/FrozenStringLiteralComment:
32+
EnforcedStyle: always_true
33+
34+
Layout/LineLength:
35+
Max: 300
36+
37+
Lint/SuppressedException:
38+
Exclude:
39+
- examples/**

.rubocop_todo.yml

-50
This file was deleted.

Guardfile

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# frozen_string_literal: true
2+
3+
# A sample Guardfile
4+
# More info at https://github.com/guard/guard#readme
5+
6+
## Uncomment and set this to only include directories you want to watch
7+
# directories %w(app lib config test spec features) \
8+
# .select{|d| Dir.exist?(d) ? d : UI.warning("Directory #{d} does not exist")}
9+
10+
## Note: if you are using the `directories` clause above and you are not
11+
## watching the project directory ('.'), then you will want to move
12+
## the Guardfile to a watched dir and symlink it back, e.g.
13+
#
14+
# $ mkdir config
15+
# $ mv Guardfile config/
16+
# $ ln -s config/Guardfile .
17+
#
18+
# and, you'll have to watch "config/Guardfile" instead of "Guardfile"
19+
20+
guard :rubocop, cli: '-a' do
21+
watch(/.+\.rb$/)
22+
watch(%r{(?:.+/)?\.rubocop(?:_todo)?\.yml$}) { |m| File.dirname(m[0]) }
23+
end

examples/client_initialization.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require_relative '../lib/typesense'
44
require 'awesome_print'
5+
require 'logger'
56

67
AwesomePrint.defaults = {
78
indent: -2
@@ -62,6 +63,6 @@
6263
healthcheck_interval_seconds: 1,
6364
retry_interval_seconds: 0.01,
6465
connection_timeout_seconds: 10,
65-
logger: Logger.new(STDOUT),
66+
logger: Logger.new($stdout),
6667
log_level: Logger::DEBUG
6768
)

lib/typesense/api_call.rb

+43-41
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
# frozen_string_literal: true
22

3-
require 'httparty'
3+
require 'typhoeus'
4+
require 'oj'
45

56
module Typesense
67
class ApiCall
7-
include HTTParty
8-
98
API_KEY_HEADER_NAME = 'X-TYPESENSE-API-KEY'
109

1110
def initialize(configuration)
@@ -30,38 +29,38 @@ def post(endpoint, parameters = {})
3029

3130
perform_request :post,
3231
endpoint,
33-
body: body,
34-
headers: default_headers.merge(headers)
32+
headers,
33+
body: body
3534
end
3635

3736
def put(endpoint, parameters = {})
3837
headers, body = extract_headers_and_body_from(parameters)
3938

4039
perform_request :put,
4140
endpoint,
42-
body: body,
43-
headers: default_headers.merge(headers)
41+
headers,
42+
body: body
4443
end
4544

4645
def get(endpoint, parameters = {})
4746
headers, query = extract_headers_and_query_from(parameters)
4847

4948
perform_request :get,
5049
endpoint,
51-
query: query,
52-
headers: default_headers.merge(headers)
50+
headers,
51+
params: query
5352
end
5453

5554
def delete(endpoint, parameters = {})
5655
headers, query = extract_headers_and_query_from(parameters)
5756

5857
perform_request :delete,
5958
endpoint,
60-
query: query,
61-
headers: default_headers.merge(headers)
59+
headers,
60+
params: query
6261
end
6362

64-
def perform_request(method, endpoint, options = {})
63+
def perform_request(method, endpoint, headers = {}, options = {})
6564
@configuration.validate!
6665
last_exception = nil
6766
@logger.debug "Performing #{method.to_s.upcase} request: #{endpoint}"
@@ -71,24 +70,31 @@ def perform_request(method, endpoint, options = {})
7170
@logger.debug "Attempting #{method.to_s.upcase} request Try ##{num_tries} to Node #{node[:index]}"
7271

7372
begin
74-
response_object = self.class.send(method,
75-
uri_for(endpoint, node),
76-
default_options.merge(options))
77-
response_code = response_object.response.code.to_i
78-
set_node_healthcheck(node, is_healthy: true) if response_code >= 1 && response_code <= 499
79-
80-
@logger.debug "Request to Node #{node[:index]} was successfully made (at the network layer). Response Code was #{response_code}."
73+
response = Typhoeus::Request.new(uri_for(endpoint, node),
74+
{
75+
method: method,
76+
headers: default_headers.merge(headers),
77+
timeout: @connection_timeout_seconds
78+
}.merge(options)).run
79+
set_node_healthcheck(node, is_healthy: true) if response.code >= 1 && response.code <= 499
80+
81+
@logger.debug "Request to Node #{node[:index]} was successfully made (at the network layer). Response Code was #{response.code}."
82+
83+
parsed_response = if response.headers && (response.headers['content-type'] || '').include?('application/json')
84+
Oj.load(response.body)
85+
else
86+
response.body
87+
end
8188

8289
# If response is 2xx return the object, else raise the response as an exception
83-
return response_object.parsed_response if response_object.response.code_type <= Net::HTTPSuccess # 2xx
90+
return parsed_response if response.code >= 200 && response.code <= 299
8491

85-
exception_message = (response_object.parsed_response && response_object.parsed_response['message']) || 'Error'
86-
raise custom_exception_klass_for(response_object.response), exception_message
87-
rescue SocketError, Net::ReadTimeout, Net::OpenTimeout,
88-
EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError,
92+
exception_message = (parsed_response && parsed_response['message']) || 'Error'
93+
raise custom_exception_klass_for(response), exception_message
94+
rescue SocketError, EOFError,
8995
Errno::EINVAL, Errno::ENETDOWN, Errno::ENETUNREACH, Errno::ENETRESET, Errno::ECONNABORTED, Errno::ECONNRESET,
9096
Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::EHOSTDOWN, Errno::EHOSTUNREACH,
91-
Timeout::Error, HTTParty::ResponseError, Typesense::Error::ServerError, Typesense::Error::HTTPStatus0Error => e
97+
Typesense::Error::TimeoutError, Typesense::Error::ServerError, Typesense::Error::HTTPStatus0Error => e
9298
# Rescue network layer exceptions and HTTP 5xx errors, so the loop can continue.
9399
# Using loops for retries instead of rescue...retry to maintain consistency with client libraries in
94100
# other languages that might not support the same construct.
@@ -108,7 +114,7 @@ def perform_request(method, endpoint, options = {})
108114
def extract_headers_and_body_from(parameters)
109115
if json_request?(parameters)
110116
headers = { 'Content-Type' => 'application/json' }
111-
body = sanitize_parameters(parameters).to_json
117+
body = Oj.dump(sanitize_parameters(parameters))
112118
else
113119
headers = {}
114120
body = parameters[:body]
@@ -199,35 +205,31 @@ def set_node_healthcheck(node, is_healthy:)
199205
end
200206

201207
def custom_exception_klass_for(response)
202-
response_code_type = response.code_type
203-
if response_code_type <= Net::HTTPBadRequest # 400
208+
if response.code == 400
204209
Typesense::Error::RequestMalformed
205-
elsif response_code_type <= Net::HTTPUnauthorized # 401
210+
elsif response.code == 401
206211
Typesense::Error::RequestUnauthorized
207-
elsif response_code_type <= Net::HTTPNotFound # 404
212+
elsif response.code == 404
208213
Typesense::Error::ObjectNotFound
209-
elsif response_code_type <= Net::HTTPConflict # 409
214+
elsif response.code == 409
210215
Typesense::Error::ObjectAlreadyExists
211-
elsif response_code_type <= Net::HTTPUnprocessableEntity # 422
216+
elsif response.code == 422
212217
Typesense::Error::ObjectUnprocessable
213-
elsif response_code_type <= Net::HTTPServerError # 5xx
218+
elsif response.code >= 500 && response.code <= 599
214219
Typesense::Error::ServerError
215-
elsif response.code.to_i.zero?
220+
elsif response.timed_out?
221+
Typesense::Error::TimeoutError
222+
elsif response.code.zero?
216223
Typesense::Error::HTTPStatus0Error
217224
else
218225
Typesense::Error::HTTPError
219226
end
220227
end
221228

222-
def default_options
223-
{
224-
timeout: @connection_timeout_seconds
225-
}
226-
end
227-
228229
def default_headers
229230
{
230-
API_KEY_HEADER_NAME.to_s => @api_key
231+
API_KEY_HEADER_NAME.to_s => @api_key,
232+
'User-Agent' => 'Typesense Ruby Client'
231233
}
232234
end
233235
end

lib/typesense/configuration.rb

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require 'logger'
4+
35
module Typesense
46
class Configuration
57
attr_accessor :nodes, :nearest_node, :connection_timeout_seconds, :healthcheck_interval_seconds, :num_retries, :retry_interval_seconds, :api_key, :logger, :log_level
@@ -13,7 +15,7 @@ def initialize(options = {})
1315
@retry_interval_seconds = options[:retry_interval_seconds] || 0.1
1416
@api_key = options[:api_key]
1517

16-
@logger = options[:logger] || Logger.new(STDOUT)
18+
@logger = options[:logger] || Logger.new($stdout)
1719
@log_level = options[:log_level] || Logger::WARN
1820
@logger.level = @log_level
1921

lib/typesense/documents.rb

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require 'oj'
4+
35
module Typesense
46
class Documents
57
RESOURCE_PATH = '/documents'
@@ -15,7 +17,7 @@ def create(document)
1517
end
1618

1719
def create_many(documents)
18-
documents_in_jsonl_format = documents.map { |document| JSON.dump(document) }.join("\n")
20+
documents_in_jsonl_format = documents.map { |document| Oj.dump(document) }.join("\n")
1921
import(documents_in_jsonl_format)
2022
end
2123

@@ -38,7 +40,7 @@ def [](document_id)
3840
private
3941

4042
def endpoint_path(operation = nil)
41-
"#{Collections::RESOURCE_PATH}/#{@collection_name}#{Documents::RESOURCE_PATH}#{operation.nil? ? '' : '/' + operation}"
43+
"#{Collections::RESOURCE_PATH}/#{@collection_name}#{Documents::RESOURCE_PATH}#{operation.nil? ? '' : "/#{operation}"}"
4244
end
4345
end
4446
end

lib/typesense/error.rb

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ class ServerError < Error
2626
class HTTPStatus0Error < Error
2727
end
2828

29+
class TimeoutError < Error
30+
end
31+
2932
class NoMethodError < ::NoMethodError
3033
end
3134

lib/typesense/overrides.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def [](override_id)
2525
private
2626

2727
def endpoint_path(operation = nil)
28-
"#{Collections::RESOURCE_PATH}/#{@collection_name}#{Overrides::RESOURCE_PATH}#{operation.nil? ? '' : '/' + operation}"
28+
"#{Collections::RESOURCE_PATH}/#{@collection_name}#{Overrides::RESOURCE_PATH}#{operation.nil? ? '' : "/#{operation}"}"
2929
end
3030
end
3131
end

spec/typesense/alias_spec.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
require_relative 'shared_configuration_context'
55

66
describe Typesense::Alias do
7-
include_context 'with Typesense configuration'
8-
97
subject(:books_alias) { typesense.aliases['books'] }
108

9+
include_context 'with Typesense configuration'
10+
1111
describe '#retrieve' do
1212
it 'returns the specified alias' do
1313
stub_request(:get, Typesense::ApiCall.new(typesense.configuration).send(:uri_for, '/aliases/books', typesense.configuration.nodes[0]))

spec/typesense/aliases_spec.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
require_relative 'shared_configuration_context'
55

66
describe Typesense::Aliases do
7-
include_context 'with Typesense configuration'
8-
97
subject(:aliases) { typesense.aliases }
108

9+
include_context 'with Typesense configuration'
10+
1111
describe '#upsert' do
1212
it 'upserts an alias and returns it' do
1313
stub_request(:put, Typesense::ApiCall.new(typesense.configuration).send(:uri_for, '/aliases/books', typesense.configuration.nodes[0]))

0 commit comments

Comments
 (0)