From 81072f205bcfb5c8b1b4e2e70015b19c1c37980b Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Mon, 27 Apr 2026 18:03:35 +0100 Subject: [PATCH 1/3] chore: small consistency and documentation fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AnalyticsRules and CurationSets now memoize their resource proxies in `[]`, matching the pattern every other collection class already uses (Collections, Documents, Aliases, Keys, Presets, SynonymSets, etc). Calls like `client.curation_sets['x']` now return the same instance across calls, which is the documented contract elsewhere. - Remove an unreachable `|| 3` fallback from `Configuration#num_retries`: the LHS is always an integer because `validate!` rejects empty `nodes`. - Replace `node.send(:[], attr)` with `node[attr]` in `Configuration#node_missing_parameters?` — same behaviour, less indirection. - Document `Typesense::Error#data` in the README so callers know how to read the HTTP status and body from a raised exception. --- README.md | 16 ++++++++++++++++ lib/typesense/analytics_rules.rb | 3 ++- lib/typesense/configuration.rb | 4 ++-- lib/typesense/curation_sets.rb | 3 ++- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a337a88..f924f11 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,22 @@ Here are some examples with inline comments that walk you through how to use the Tests are also a good place to know how the the library works internally: [spec](spec) +### Error handling + +Failed requests raise a subclass of `Typesense::Error`. The exception's `message` is the API error string (`"Object Not Found"`, etc.), and the underlying Faraday response is available on `#data` for callers that need the HTTP status, headers, or raw body: + +```ruby +begin + client.collections['unknown'].retrieve +rescue Typesense::Error::ObjectNotFound => e + e.message # => "Not Found" + e.data.status # => 404 + e.data.body # => raw response body +end +``` + +The exception classes that map to specific status codes are: `RequestMalformed` (400), `RequestUnauthorized` (401), `ObjectNotFound` (404), `ObjectAlreadyExists` (409), `ObjectUnprocessable` (422), `ServerError` (5xx), `HTTPStatus0Error` (status 0), `TimeoutError`, and `HTTPError` (anything else). + ## Compatibility | Typesense Server | typesense-ruby | diff --git a/lib/typesense/analytics_rules.rb b/lib/typesense/analytics_rules.rb index 6d1b169..f5a4996 100644 --- a/lib/typesense/analytics_rules.rb +++ b/lib/typesense/analytics_rules.rb @@ -6,6 +6,7 @@ class AnalyticsRules def initialize(api_call) @api_call = api_call + @analytics_rules = {} end def create(rules) @@ -17,7 +18,7 @@ def retrieve end def [](rule_name) - AnalyticsRule.new(rule_name, @api_call) + @analytics_rules[rule_name] ||= AnalyticsRule.new(rule_name, @api_call) end end end diff --git a/lib/typesense/configuration.rb b/lib/typesense/configuration.rb index 4b57bcf..a044c9f 100644 --- a/lib/typesense/configuration.rb +++ b/lib/typesense/configuration.rb @@ -11,7 +11,7 @@ def initialize(options = {}) @nearest_node = options[:nearest_node] @connection_timeout_seconds = options[:connection_timeout_seconds] || options[:timeout_seconds] || 10 @healthcheck_interval_seconds = options[:healthcheck_interval_seconds] || 15 - @num_retries = options[:num_retries] || (@nodes.length + (@nearest_node.nil? ? 0 : 1)) || 3 + @num_retries = options[:num_retries] || (@nodes.length + (@nearest_node.nil? ? 0 : 1)) @retry_interval_seconds = options[:retry_interval_seconds] || 0.1 @api_key = options[:api_key] @@ -36,7 +36,7 @@ def validate! private def node_missing_parameters?(node) - %i[protocol host port].any? { |attr| node.send(:[], attr).nil? } + %i[protocol host port].any? { |attr| node[attr].nil? } end def show_deprecation_warnings(options) diff --git a/lib/typesense/curation_sets.rb b/lib/typesense/curation_sets.rb index a24a655..10e8857 100644 --- a/lib/typesense/curation_sets.rb +++ b/lib/typesense/curation_sets.rb @@ -6,6 +6,7 @@ class CurationSets def initialize(api_call) @api_call = api_call + @curation_sets = {} end def upsert(curation_set_name, curation_set_data) @@ -17,7 +18,7 @@ def retrieve end def [](curation_set_name) - CurationSet.new(curation_set_name, @api_call) + @curation_sets[curation_set_name] ||= CurationSet.new(curation_set_name, @api_call) end end end From 69edc94cfd5544e52c977a8a68998300dd19ac91 Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Tue, 28 Apr 2026 09:26:07 +0100 Subject: [PATCH 2/3] Update analytics_rules_spec.rb --- spec/typesense/analytics_rules_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/typesense/analytics_rules_spec.rb b/spec/typesense/analytics_rules_spec.rb index bdc7d9c..adc31f7 100644 --- a/spec/typesense/analytics_rules_spec.rb +++ b/spec/typesense/analytics_rules_spec.rb @@ -147,10 +147,10 @@ expect(result.instance_variable_get(:@rule_name)).to eq(rule_name) end - it 'does not memoize the analytics rule instance' do + it 'memoizes the analytics rule instance' do first_call = integration_client.analytics.rules[rule_name] second_call = integration_client.analytics.rules[rule_name] - expect(first_call).not_to equal(second_call) + expect(first_call).to equal(second_call) end end end From bf0f64bf4079b1926b4b4a916ea98fe45a9e4bed Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Wed, 29 Apr 2026 10:26:21 +0100 Subject: [PATCH 3/3] Update configuration_spec.rb --- spec/typesense/configuration_spec.rb | 43 ++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/spec/typesense/configuration_spec.rb b/spec/typesense/configuration_spec.rb index de99375..8c08768 100644 --- a/spec/typesense/configuration_spec.rb +++ b/spec/typesense/configuration_spec.rb @@ -29,4 +29,47 @@ end end end + + describe '#num_retries default' do + let(:base_options) do + { + api_key: 'abcd', + nodes: [ + { host: 'node0', port: 8108, protocol: 'http' }, + { host: 'node1', port: 8108, protocol: 'http' }, + { host: 'node2', port: 8108, protocol: 'http' } + ], + log_level: Logger::ERROR + } + end + + it 'defaults to the number of nodes when no nearest_node is set' do + config = described_class.new(base_options) + expect(config.num_retries).to eq(3) + end + + it 'defaults to nodes.length + 1 when a nearest_node is set' do + config = described_class.new( + base_options.merge(nearest_node: { host: 'nearestNode', port: 6108, protocol: 'http' }) + ) + expect(config.num_retries).to eq(4) + end + + it 'reflects node count for single-node setups' do + config = described_class.new( + base_options.merge(nodes: [{ host: 'node0', port: 8108, protocol: 'http' }]) + ) + expect(config.num_retries).to eq(1) + end + + it 'honors an explicit num_retries option' do + config = described_class.new(base_options.merge(num_retries: 7)) + expect(config.num_retries).to eq(7) + end + + it 'honors an explicit num_retries of 0 (Integer is truthy in Ruby)' do + config = described_class.new(base_options.merge(num_retries: 0)) + expect(config.num_retries).to eq(0) + end + end end