diff --git a/lib/typesense.rb b/lib/typesense.rb index c05ccdb..a14dda5 100644 --- a/lib/typesense.rb +++ b/lib/typesense.rb @@ -22,8 +22,13 @@ module Typesense require_relative 'typesense/key' require_relative 'typesense/multi_search' require_relative 'typesense/analytics' +require_relative 'typesense/analytics_events' require_relative 'typesense/analytics_rules' require_relative 'typesense/analytics_rule' +require_relative 'typesense/analytics_v1' +require_relative 'typesense/analytics_events_v1' +require_relative 'typesense/analytics_rules_v1' +require_relative 'typesense/analytics_rule_v1' require_relative 'typesense/presets' require_relative 'typesense/preset' require_relative 'typesense/debug' diff --git a/lib/typesense/analytics.rb b/lib/typesense/analytics.rb index 1647a2d..8c8ed72 100644 --- a/lib/typesense/analytics.rb +++ b/lib/typesense/analytics.rb @@ -11,5 +11,9 @@ def initialize(api_call) def rules @rules ||= AnalyticsRules.new(@api_call) end + + def events + @events ||= AnalyticsEvents.new(@api_call) + end end end diff --git a/lib/typesense/analytics_events.rb b/lib/typesense/analytics_events.rb new file mode 100644 index 0000000..85b55da --- /dev/null +++ b/lib/typesense/analytics_events.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Typesense + class AnalyticsEvents + RESOURCE_PATH = '/analytics/events' + + def initialize(api_call) + @api_call = api_call + end + + def create(params) + @api_call.post(self.class::RESOURCE_PATH, params) + end + + def retrieve(params = {}) + @api_call.get(self.class::RESOURCE_PATH, params) + end + end +end diff --git a/lib/typesense/analytics_events_v1.rb b/lib/typesense/analytics_events_v1.rb new file mode 100644 index 0000000..645ee69 --- /dev/null +++ b/lib/typesense/analytics_events_v1.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Typesense + class AnalyticsEventsV1 + RESOURCE_PATH = '/analytics/events' + + def initialize(api_call) + @api_call = api_call + end + + def create(params) + @api_call.post(endpoint_path, params) + end + + private + + def endpoint_path(operation = nil) + "#{AnalyticsEventsV1::RESOURCE_PATH}#{"/#{URI.encode_www_form_component(operation)}" unless operation.nil?}" + end + end +end diff --git a/lib/typesense/analytics_rule.rb b/lib/typesense/analytics_rule.rb index 5f1f501..bf1d058 100644 --- a/lib/typesense/analytics_rule.rb +++ b/lib/typesense/analytics_rule.rb @@ -15,6 +15,10 @@ def delete @api_call.delete(endpoint_path) end + def update(params) + @api_call.put(endpoint_path, params) + end + private def endpoint_path diff --git a/lib/typesense/analytics_rule_v1.rb b/lib/typesense/analytics_rule_v1.rb new file mode 100644 index 0000000..376cf9b --- /dev/null +++ b/lib/typesense/analytics_rule_v1.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Typesense + class AnalyticsRuleV1 + def initialize(rule_name, api_call) + @rule_name = rule_name + @api_call = api_call + end + + def retrieve + @api_call.get(endpoint_path) + end + + def delete + @api_call.delete(endpoint_path) + end + + private + + def endpoint_path + "#{AnalyticsRulesV1::RESOURCE_PATH}/#{URI.encode_www_form_component(@rule_name)}" + end + end +end diff --git a/lib/typesense/analytics_rules.rb b/lib/typesense/analytics_rules.rb index d54a3de..7c0bb43 100644 --- a/lib/typesense/analytics_rules.rb +++ b/lib/typesense/analytics_rules.rb @@ -5,26 +5,27 @@ class AnalyticsRules RESOURCE_PATH = '/analytics/rules' def initialize(api_call) - @api_call = api_call - @analytics_rules = {} + @api_call = api_call end - def upsert(rule_name, params) - @api_call.put(endpoint_path(rule_name), params) + def create(rules) + @api_call.post(self.class::RESOURCE_PATH, rules) end def retrieve - @api_call.get(endpoint_path) + @api_call.get(self.class::RESOURCE_PATH) end def [](rule_name) - @analytics_rules[rule_name] ||= AnalyticsRule.new(rule_name, @api_call) + AnalyticsRule.new(rule_name, @api_call) end - private + def respond_to_missing?(_method_name, _include_private = false) + true + end - def endpoint_path(operation = nil) - "#{AnalyticsRules::RESOURCE_PATH}#{"/#{URI.encode_www_form_component(operation)}" unless operation.nil?}" + def method_missing(method_name, *_args, &_block) + self[method_name.to_s] end end end diff --git a/lib/typesense/analytics_rules_v1.rb b/lib/typesense/analytics_rules_v1.rb new file mode 100644 index 0000000..0cb1ec5 --- /dev/null +++ b/lib/typesense/analytics_rules_v1.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Typesense + class AnalyticsRulesV1 + RESOURCE_PATH = '/analytics/rules' + + def initialize(api_call) + @api_call = api_call + @analytics_rules = {} + end + + def upsert(rule_name, params) + @api_call.put(endpoint_path(rule_name), params) + end + + def retrieve + @api_call.get(endpoint_path) + end + + def [](rule_name) + @analytics_rules[rule_name] ||= AnalyticsRuleV1.new(rule_name, @api_call) + end + + private + + def endpoint_path(operation = nil) + "#{AnalyticsRulesV1::RESOURCE_PATH}#{"/#{URI.encode_www_form_component(operation)}" unless operation.nil?}" + end + end +end diff --git a/lib/typesense/analytics_v1.rb b/lib/typesense/analytics_v1.rb new file mode 100644 index 0000000..c65f2cc --- /dev/null +++ b/lib/typesense/analytics_v1.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Typesense + class AnalyticsV1 + RESOURCE_PATH = '/analytics' + + def initialize(api_call) + @api_call = api_call + end + + def rules + @rules ||= AnalyticsRulesV1.new(@api_call) + end + + def events + @events ||= AnalyticsEventsV1.new(@api_call) + end + end +end diff --git a/lib/typesense/client.rb b/lib/typesense/client.rb index e7ef103..1f0d202 100644 --- a/lib/typesense/client.rb +++ b/lib/typesense/client.rb @@ -3,7 +3,7 @@ module Typesense class Client attr_reader :configuration, :collections, :aliases, :keys, :debug, :health, :metrics, :stats, :operations, - :multi_search, :analytics, :presets, :stemming, :nl_search_models + :multi_search, :analytics, :analytics_v1, :presets, :stemming, :nl_search_models def initialize(options = {}) @configuration = Configuration.new(options) @@ -18,6 +18,7 @@ def initialize(options = {}) @stats = Stats.new(@api_call) @operations = Operations.new(@api_call) @analytics = Analytics.new(@api_call) + @analytics_v1 = AnalyticsV1.new(@api_call) @stemming = Stemming.new(@api_call) @presets = Presets.new(@api_call) @nl_search_models = NlSearchModels.new(@api_call) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 942b30f..42e2179 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -34,6 +34,37 @@ def typesense_healthy?(host = 'localhost', port = 8108) false end +def typesense_version + WebMock.allow_net_connect! + conn = Faraday.new('http://localhost:8108') + response = conn.get('/debug') do |req| + req.headers['X-TYPESENSE-API-KEY'] = 'xyz' + end + + if response.status == 200 && !response.body.empty? + debug_info = JSON.parse(response.body) + debug_info['version'] + end +rescue StandardError + nil +ensure + WebMock.disable_net_connect!(allow_localhost: true) +end + +def typesense_v30_or_above? + version = typesense_version + return false unless version + + return true if version == 'nightly' + + if version.match(/^v(\d+)/) + major_version = Regexp.last_match(1).to_i + return major_version >= 30 + end + + false +end + def ensure_typesense_running if typesense_healthy? puts '✅ Typesense is already running and healthy, ready for use in integration tests' @@ -96,7 +127,6 @@ def stop_typesense_if_started config.before(:suite) do ensure_typesense_running - WebMock.disable_net_connect! end config.after(:suite) do diff --git a/spec/typesense/analytics_events_spec.rb b/spec/typesense/analytics_events_spec.rb new file mode 100644 index 0000000..1503215 --- /dev/null +++ b/spec/typesense/analytics_events_spec.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true + +require_relative '../spec_helper' +require_relative 'shared_configuration_context' + +describe Typesense::AnalyticsEvents do + subject(:analytics_events) { typesense.analytics.events } + + include_context 'with Typesense configuration' + + let(:rule_name) { 'test__rule' } + let(:rule_configuration) do + { + 'name' => rule_name, + 'type' => 'counter', + 'collection' => 'test_products', + 'event_type' => 'click', + 'rule_tag' => 'test_tag', + 'params' => { + 'counter_field' => 'popularity', + 'weight' => 1 + } + } + end + + let(:integration_client) do + Typesense::Client.new( + nodes: [{ + host: 'localhost', + port: '8108', + protocol: 'http' + }], + api_key: 'xyz', + connection_timeout_seconds: 10 + ) + end + + before do + skip('New Analytics API is not supported in Typesense 29.0 and below') unless typesense_v30_or_above? + + WebMock.disable! + + begin + integration_client.collections.create({ + 'name' => 'test_products', + 'fields' => [ + { 'name' => 'company_name', 'type' => 'string' }, + { 'name' => 'num_employees', 'type' => 'int32' }, + { 'name' => 'country', 'type' => 'string', 'facet' => true }, + { 'name' => 'popularity', 'type' => 'int32', 'optional' => true } + ], + 'default_sorting_field' => 'num_employees' + }) + rescue Typesense::Error::ObjectAlreadyExists + # Collection already exists, which is fine + end + + begin + integration_client.analytics.rules.create([rule_configuration]) + rescue StandardError + # Rule creation might fail, which is fine for testing + end + end + + after do + begin + rules = integration_client.analytics.rules.retrieve + if rules.is_a?(Array) + rules.each do |rule| + next unless rule['name'].to_s.start_with?('test__') + + begin + integration_client.analytics.rules[rule['name']].delete + rescue StandardError + # Ignore cleanup errors + end + end + end + rescue StandardError + # Ignore cleanup errors + end + + begin + integration_client.collections['test_products'].delete + rescue StandardError + # Ignore cleanup errors + end + + WebMock.enable! + end + + describe '#create' do + it 'creates an analytics event and returns it' do + event = { + 'name' => rule_name, + 'event_type' => 'click', + 'data' => { + 'doc_id' => '1', + 'user_id' => 'test_user' + } + } + + result = integration_client.analytics.events.create(event) + expect(result).to be_a(Hash) + end + end + + describe '#retrieve' do + it 'retrieves analytics events with query parameters' do + event = { + 'name' => rule_name, + 'event_type' => 'click', + 'data' => { + 'doc_id' => '1', + 'user_id' => 'test_user' + } + } + + integration_client.analytics.events.create(event) + + result = integration_client.analytics.events.retrieve({ + 'user_id' => 'test_user', + 'name' => rule_name, + 'n' => 10 + }) + + expect(result).to be_a(Hash) + expect(result['events']).to be_a(Array) + end + end + + describe 'event creation with different event types' do + it 'creates click and conversion events' do + click_event = { + 'name' => rule_name, + 'event_type' => 'click', + 'data' => { + 'doc_id' => '1', + 'user_id' => 'test_user' + } + } + + conversion_event = { + 'name' => rule_name, + 'event_type' => 'conversion', + 'data' => { + 'doc_id' => '1', + 'user_id' => 'test_user' + } + } + + click_response = integration_client.analytics.events.create(click_event) + expect(click_response).to be_a(Hash) + + conversion_response = integration_client.analytics.events.create(conversion_event) + expect(conversion_response).to be_a(Hash) + end + end +end diff --git a/spec/typesense/analytics_rule_spec.rb b/spec/typesense/analytics_rule_spec.rb index 2ae99a7..18bcef5 100644 --- a/spec/typesense/analytics_rule_spec.rb +++ b/spec/typesense/analytics_rule_spec.rb @@ -4,48 +4,135 @@ require_relative 'shared_configuration_context' describe Typesense::AnalyticsRule do - subject(:analytics_rule) { typesense.analytics.rules['search_suggestions'] } + subject(:analytics_rule) { typesense.analytics.rules[rule_name] } include_context 'with Typesense configuration' - let(:analytics_rule_data) do + let(:rule_name) { 'test__rule' } + let(:rule_configuration) do { - 'name' => 'search_suggestions', - 'type' => 'popular_queries', + 'name' => rule_name, + 'type' => 'counter', + 'collection' => 'test_products', + 'event_type' => 'click', + 'rule_tag' => 'test_tag', 'params' => { - 'source' => { 'collections' => ['products'] }, - 'destination' => { 'collection' => 'products_top_queries' }, - 'limit' => 100 + 'counter_field' => 'popularity', + 'weight' => 1 } } end + let(:integration_client) do + Typesense::Client.new( + nodes: [{ + host: 'localhost', + port: '8108', + protocol: 'http' + }], + api_key: 'xyz', + connection_timeout_seconds: 10 + ) + end + + before do + skip('New Analytics API is not supported in Typesense 29.0 and below') unless typesense_v30_or_above? + + WebMock.disable! + + # Create test collection + begin + integration_client.collections.create({ + 'name' => 'test_products', + 'fields' => [ + { 'name' => 'company_name', 'type' => 'string' }, + { 'name' => 'num_employees', 'type' => 'int32' }, + { 'name' => 'country', 'type' => 'string', 'facet' => true }, + { 'name' => 'popularity', 'type' => 'int32', 'optional' => true } + ], + 'default_sorting_field' => 'num_employees' + }) + rescue Typesense::Error::ObjectAlreadyExists + # Collection already exists, which is fine + end + + # Create test rule + begin + integration_client.analytics.rules.create([rule_configuration]) + rescue StandardError + # Rule creation might fail, which is fine for testing + end + end + + after do + # Clean up test rules + begin + rules = integration_client.analytics.rules.retrieve + if rules.is_a?(Array) + rules.each do |rule| + next unless rule['name'].to_s.start_with?('test__') + + begin + integration_client.analytics.rules[rule['name']].delete + rescue StandardError + # Ignore cleanup errors + end + end + end + rescue StandardError + # Ignore cleanup errors + end + + # Clean up test collection + begin + integration_client.collections['test_products'].delete + rescue StandardError + # Ignore cleanup errors + end + + WebMock.enable! + end + describe '#retrieve' do it 'returns the specified analytics rule' do - stub_request(:get, Typesense::ApiCall.new(typesense.configuration).send(:uri_for, '/analytics/rules/search_suggestions', typesense.configuration.nodes[0])) - .with(headers: { - 'X-Typesense-Api-Key' => typesense.configuration.api_key, - 'Content-Type': 'application/json' - }) - .to_return(status: 200, body: JSON.dump(analytics_rule_data), headers: { 'Content-Type': 'application/json' }) - - result = analytics_rule.retrieve + result = integration_client.analytics.rules[rule_name].retrieve - expect(result).to eq(analytics_rule_data) + expect(result['name']).to eq(rule_name) + expect(result['type']).to eq('counter') + expect(result['collection']).to eq('test_products') end end describe '#delete' do it 'deletes the specified analytics rule' do - stub_request(:delete, Typesense::ApiCall.new(typesense.configuration).send(:uri_for, '/analytics/rules/search_suggestions', typesense.configuration.nodes[0])) - .with(headers: { - 'X-Typesense-Api-Key' => typesense.configuration.api_key - }) - .to_return(status: 200, body: JSON.dump('name' => 'search_suggestions'), headers: { 'Content-Type': 'application/json' }) + result = integration_client.analytics.rules[rule_name].delete + expect(result['name']).to eq(rule_name) + + # Verify the rule is deleted + expect do + integration_client.analytics.rules[rule_name].retrieve + end.to raise_error(Typesense::Error::RequestMalformed) + end + end + + describe '#update' do + it 'updates the specified analytics rule' do + update_params = { + 'type' => 'counter', + 'collection' => 'test_products', + 'event_type' => 'click', + 'rule_tag' => 'updated_tag', + 'params' => { + 'counter_field' => 'popularity', + 'weight' => 5 + } + } - result = analytics_rule.delete + result = integration_client.analytics.rules[rule_name].update(update_params) - expect(result).to eq('name' => 'search_suggestions') + expect(result['name']).to eq(rule_name) + expect(result['rule_tag']).to eq('updated_tag') + expect(result['params']['weight']).to eq(5) end end end diff --git a/spec/typesense/analytics_rule_v1_spec.rb b/spec/typesense/analytics_rule_v1_spec.rb new file mode 100644 index 0000000..0b7bd0c --- /dev/null +++ b/spec/typesense/analytics_rule_v1_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +require_relative '../spec_helper' +require_relative 'shared_configuration_context' + +describe Typesense::AnalyticsRuleV1 do + subject(:analytics_rule_v1) { typesense.analytics_v1.rules[rule_name] } + + include_context 'with Typesense configuration' + + let(:rule_name) { 'test_rule' } + let(:rule_configuration) do + { + 'type' => 'popular_queries', + 'params' => { + 'source' => { 'collections' => ['products'] }, + 'destination' => { 'collection' => 'product_queries' }, + 'expand_query' => false, + 'limit' => 100 + } + } + end + + let(:integration_client) do + Typesense::Client.new( + nodes: [{ + host: 'localhost', + port: '8108', + protocol: 'http' + }], + api_key: 'xyz', + connection_timeout_seconds: 10 + ) + end + + before do + skip('Analytics is deprecated in Typesense v30+') if typesense_v30_or_above? + + WebMock.disable! + + # Create test collection for v1 analytics + begin + integration_client.collections.create({ + 'name' => 'products', + 'fields' => [ + { 'name' => 'title', 'type' => 'string' }, + { 'name' => 'popularity', 'type' => 'int32', 'optional' => true } + ] + }) + rescue Typesense::Error::ObjectAlreadyExists + # Collection already exists, which is fine + end + + # Create test rule + begin + integration_client.analytics_v1.rules.upsert(rule_name, rule_configuration) + rescue StandardError + # Rule creation might fail, which is fine for testing + end + end + + after do + # Clean up test rules + begin + rules = integration_client.analytics_v1.rules.retrieve + if rules.is_a?(Hash) && rules['rules'] + rules['rules'].each do |rule| + integration_client.analytics_v1.rules[rule['name']].delete + rescue StandardError + # Ignore cleanup errors + end + end + rescue StandardError + # Ignore cleanup errors + end + + # Clean up test collection + begin + integration_client.collections['products'].delete + rescue StandardError + # Ignore cleanup errors + end + + WebMock.enable! + end + + describe '#retrieve' do + it 'returns the specified analytics rule' do + result = integration_client.analytics_v1.rules[rule_name].retrieve + + expect(result['name']).to eq(rule_name) + expect(result['type']).to eq('popular_queries') + end + end + + describe '#delete' do + it 'deletes the specified analytics rule' do + result = integration_client.analytics_v1.rules[rule_name].delete + expect(result['name']).to eq(rule_name) + + # Verify the rule is deleted + expect do + integration_client.analytics_v1.rules[rule_name].retrieve + end.to raise_error(Typesense::Error::ObjectNotFound) + end + end +end diff --git a/spec/typesense/analytics_rules_spec.rb b/spec/typesense/analytics_rules_spec.rb index af72806..ba74d6a 100644 --- a/spec/typesense/analytics_rules_spec.rb +++ b/spec/typesense/analytics_rules_spec.rb @@ -8,55 +8,158 @@ include_context 'with Typesense configuration' - let(:analytics_rule) do + let(:rule_name) { 'test__rule' } + let(:rule_configuration) do { - 'name' => 'search_suggestions', - 'type' => 'popular_queries', + 'name' => rule_name, + 'type' => 'counter', + 'collection' => 'test_products', + 'event_type' => 'click', + 'rule_tag' => 'test_tag', 'params' => { - 'source' => { 'collections' => ['products'] }, - 'destination' => { 'collection' => 'products_top_queries' }, - 'limit' => 100 + 'counter_field' => 'popularity', + 'weight' => 1 } } end - describe '#upsert' do - it 'creates a rule and returns it' do - stub_request(:put, Typesense::ApiCall.new(typesense.configuration).send(:uri_for, '/analytics/rules/search_suggestions', typesense.configuration.nodes[0])) - .with(body: analytics_rule, - headers: { - 'X-Typesense-Api-Key' => typesense.configuration.api_key, - 'Content-Type' => 'application/json' - }) - .to_return(status: 201, body: JSON.dump(analytics_rule), headers: { 'Content-Type': 'application/json' }) + let(:integration_client) do + Typesense::Client.new( + nodes: [{ + host: 'localhost', + port: '8108', + protocol: 'http' + }], + api_key: 'xyz', + connection_timeout_seconds: 10 + ) + end + + before do + skip('New Analytics API is not supported in Typesense 29.0 and below') unless typesense_v30_or_above? - result = typesense.analytics.rules.upsert(analytics_rule['name'], analytics_rule) + WebMock.disable! + + begin + integration_client.collections.create({ + 'name' => 'test_products', + 'fields' => [ + { 'name' => 'company_name', 'type' => 'string' }, + { 'name' => 'num_employees', 'type' => 'int32' }, + { 'name' => 'country', 'type' => 'string', 'facet' => true }, + { 'name' => 'popularity', 'type' => 'int32', 'optional' => true } + ], + 'default_sorting_field' => 'num_employees' + }) + rescue Typesense::Error::ObjectAlreadyExists + # Collection already exists, which is fine + end - expect(result).to eq(analytics_rule) + begin + integration_client.analytics.rules.create([rule_configuration]) + rescue StandardError + # Rule creation might fail, which is fine for testing + end + end + + after do + begin + rules = integration_client.analytics.rules.retrieve + if rules.is_a?(Array) + rules.each do |rule| + next unless rule['name'].to_s.start_with?('test__') + + begin + integration_client.analytics.rules[rule['name']].delete + rescue StandardError + # Ignore cleanup errors + end + end + end + rescue StandardError + # Ignore cleanup errors + end + + begin + integration_client.collections['test_products'].delete + rescue StandardError + # Ignore cleanup errors + end + + WebMock.enable! + end + + describe '#create' do + it 'creates multiple rules and returns them' do + rules = [ + { + 'name' => 'test_rule_1', + 'type' => 'counter', + 'collection' => 'test_products', + 'event_type' => 'click', + 'rule_tag' => 'test_tag', + 'params' => { + 'counter_field' => 'popularity', + 'weight' => 1 + } + }, + { + 'name' => 'test_rule_2', + 'type' => 'counter', + 'collection' => 'test_products', + 'event_type' => 'conversion', + 'rule_tag' => 'test_tag', + 'params' => { + 'counter_field' => 'popularity', + 'weight' => 2 + } + } + ] + + result = integration_client.analytics.rules.create(rules) + expect(result).to be_a(Array) + + all_rules = integration_client.analytics.rules.retrieve + expect(all_rules).to be_a(Array) + + rule_names = all_rules.map { |rule| rule['name'] } + expect(rule_names).to include('test_rule_1') + expect(rule_names).to include('test_rule_2') end end describe '#retrieve' do it 'retrieves all analytics rules' do - stub_request(:get, Typesense::ApiCall.new(typesense.configuration).send(:uri_for, '/analytics/rules', typesense.configuration.nodes[0])) - .with(headers: { - 'X-Typesense-Api-Key' => typesense.configuration.api_key, - 'Content-Type' => 'application/json' - }) - .to_return(status: 201, body: JSON.dump([analytics_rule]), headers: { 'Content-Type': 'application/json' }) - - result = analytics_rules.retrieve + result = integration_client.analytics.rules.retrieve + expect(result).to be_a(Array) + expect(result.length).to be >= 1 - expect(result).to eq([analytics_rule]) + rule_names = result.map { |rule| rule['name'] } + expect(rule_names).to include(rule_name) end end describe '#[]' do it 'creates an analytics rule object and returns it' do - result = analytics_rules['search_suggestions'] + result = integration_client.analytics.rules[rule_name] + + expect(result).to be_a(Typesense::AnalyticsRule) + expect(result.instance_variable_get(:@rule_name)).to eq(rule_name) + end + + it 'does not memoize 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) + end + end + + describe '#method_missing' do + it 'allows accessing rules using method calls' do + result = integration_client.analytics.rules.send(rule_name) expect(result).to be_a(Typesense::AnalyticsRule) - expect(result.instance_variable_get(:@rule_name)).to eq('search_suggestions') + expect(result.instance_variable_get(:@rule_name)).to eq(rule_name) end end end diff --git a/spec/typesense/analytics_rules_v1_spec.rb b/spec/typesense/analytics_rules_v1_spec.rb new file mode 100644 index 0000000..de419f2 --- /dev/null +++ b/spec/typesense/analytics_rules_v1_spec.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +require_relative '../spec_helper' +require_relative 'shared_configuration_context' + +describe Typesense::AnalyticsRulesV1 do + subject(:analytics_rules_v1) { typesense.analytics_v1.rules } + + include_context 'with Typesense configuration' + + let(:rule_name) { 'test_rule' } + let(:rule_configuration) do + { + 'type' => 'popular_queries', + 'params' => { + 'source' => { 'collections' => ['products'] }, + 'destination' => { 'collection' => 'product_queries' }, + 'expand_query' => false, + 'limit' => 100 + } + } + end + + let(:integration_client) do + Typesense::Client.new( + nodes: [{ + host: 'localhost', + port: '8108', + protocol: 'http' + }], + api_key: 'xyz', + connection_timeout_seconds: 10 + ) + end + + before do + skip('Analytics is deprecated in Typesense v30+') if typesense_v30_or_above? + + WebMock.disable! + + # Create test collection for v1 analytics + begin + integration_client.collections.create({ + 'name' => 'products', + 'fields' => [ + { 'name' => 'title', 'type' => 'string' }, + { 'name' => 'popularity', 'type' => 'int32', 'optional' => true } + ] + }) + rescue Typesense::Error::ObjectAlreadyExists + # Collection already exists, which is fine + end + end + + after do + # Clean up test rules + begin + rules = integration_client.analytics_v1.rules.retrieve + if rules.is_a?(Hash) && rules['rules'] + rules['rules'].each do |rule| + integration_client.analytics_v1.rules[rule['name']].delete + rescue StandardError + # Ignore cleanup errors + end + end + rescue StandardError + # Ignore cleanup errors + end + + # Clean up test collection + begin + integration_client.collections['products'].delete + rescue StandardError + # Ignore cleanup errors + end + + WebMock.enable! + end + + describe '#upsert' do + it 'creates a rule and returns it' do + result = integration_client.analytics_v1.rules.upsert(rule_name, rule_configuration) + + expect(result['name']).to eq(rule_name) + expect(result['type']).to eq('popular_queries') + end + end + + describe '#retrieve' do + it 'retrieves all analytics rules' do + # First create a rule + integration_client.analytics_v1.rules.upsert(rule_name, rule_configuration) + + result = integration_client.analytics_v1.rules.retrieve + + expect(result).to be_a(Hash) + expect(result['rules']).to be_an(Array) + expect(result['rules'].length).to be >= 1 + end + end + + describe '#[]' do + it 'creates an analytics rule object and returns it' do + # First create a rule + integration_client.analytics_v1.rules.upsert(rule_name, rule_configuration) + + result = integration_client.analytics_v1.rules[rule_name] + + expect(result).to be_a(Typesense::AnalyticsRuleV1) + expect(result.instance_variable_get(:@rule_name)).to eq(rule_name) + end + + it 'memoizes the analytics rule instance' do + # First create a rule + integration_client.analytics_v1.rules.upsert(rule_name, rule_configuration) + + first_call = integration_client.analytics_v1.rules[rule_name] + second_call = integration_client.analytics_v1.rules[rule_name] + expect(first_call).to equal(second_call) + end + end +end diff --git a/spec/typesense/analytics_v1_spec.rb b/spec/typesense/analytics_v1_spec.rb new file mode 100644 index 0000000..1a1aa46 --- /dev/null +++ b/spec/typesense/analytics_v1_spec.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require_relative '../spec_helper' +require_relative 'shared_configuration_context' + +describe Typesense::AnalyticsV1 do + subject(:analytics_v1) { typesense.analytics_v1 } + + include_context 'with Typesense configuration' + + let(:integration_client) do + Typesense::Client.new( + nodes: [{ + host: 'localhost', + port: '8108', + protocol: 'http' + }], + api_key: 'xyz', + connection_timeout_seconds: 10 + ) + end + + before do + skip('Analytics is deprecated in Typesense v30+') if typesense_v30_or_above? + end + + describe '#rules' do + it 'returns an AnalyticsRulesV1 instance' do + expect(analytics_v1.rules).to be_a(Typesense::AnalyticsRulesV1) + end + + it 'memoizes the rules instance' do + first_call = analytics_v1.rules + second_call = analytics_v1.rules + expect(first_call).to equal(second_call) + end + end + + describe '#events' do + it 'returns an AnalyticsEventsV1 instance' do + expect(analytics_v1.events).to be_a(Typesense::AnalyticsEventsV1) + end + + it 'memoizes the events instance' do + first_call = analytics_v1.events + second_call = analytics_v1.events + expect(first_call).to equal(second_call) + end + end + + context 'with integration tests', :integration do + before do + WebMock.disable! + + # Create test collections for v1 analytics + begin + integration_client.collections.create({ + 'name' => 'products', + 'fields' => [ + { 'name' => 'title', 'type' => 'string' }, + { 'name' => 'popularity', 'type' => 'int32', 'optional' => true } + ] + }) + rescue Typesense::Error::ObjectAlreadyExists + # Collection already exists, which is fine + end + + begin + integration_client.collections.create({ + 'name' => 'product_queries', + 'fields' => [ + { 'name' => 'query', 'type' => 'string' }, + { 'name' => 'count', 'type' => 'int32' } + ] + }) + rescue Typesense::Error::ObjectAlreadyExists + # Collection already exists, which is fine + end + end + + after do + # Clean up test collections + begin + integration_client.collections['products'].delete + rescue StandardError + # Ignore cleanup errors + end + + begin + integration_client.collections['product_queries'].delete + rescue StandardError + # Ignore cleanup errors + end + + WebMock.enable! + end + + it 'can create and use analytics rules with v1 API' do + # Create a rule using v1 API + rule_config = { + 'type' => 'popular_queries', + 'params' => { + 'source' => { + 'collections' => ['products'] + }, + 'destination' => { + 'collection' => 'product_queries' + }, + 'expand_query' => false, + 'limit' => 100 + } + } + + # This should work with v1 API - name is passed separately + integration_client.analytics_v1.rules.upsert('products_popularity', rule_config) + + # Verify the rule was created + rules = integration_client.analytics_v1.rules.retrieve + expect(rules).to be_a(Hash) + expect(rules['rules']).to be_an(Array) + + # Clean up + integration_client.analytics_v1.rules['products_popularity'].delete + end + end +end diff --git a/spec/typesense/client_spec.rb b/spec/typesense/client_spec.rb index 2e86eff..854ab11 100644 --- a/spec/typesense/client_spec.rb +++ b/spec/typesense/client_spec.rb @@ -22,4 +22,20 @@ expect(result).to be_a(Typesense::Debug) end end + + describe '#analytics' do + it 'creates an analytics object and returns it' do + result = typesense.analytics + + expect(result).to be_a(Typesense::Analytics) + end + end + + describe '#analytics_v1' do + it 'creates an analytics_v1 object and returns it' do + result = typesense.analytics_v1 + + expect(result).to be_a(Typesense::AnalyticsV1) + end + end end diff --git a/spec/typesense/synonym_spec.rb b/spec/typesense/synonym_spec.rb index e3585f8..f7202ef 100644 --- a/spec/typesense/synonym_spec.rb +++ b/spec/typesense/synonym_spec.rb @@ -19,6 +19,10 @@ } end + before do + skip('Synonyms is deprecated in Typesense v30+, use SynonymSets instead') if typesense_v30_or_above? + end + describe '#retrieve' do it 'returns the specified synonym' do stub_request(:get, Typesense::ApiCall.new(typesense.configuration).send(:uri_for, '/collections/companies/synonyms/synonym-set-1', typesense.configuration.nodes[0])) diff --git a/spec/typesense/synonyms_spec.rb b/spec/typesense/synonyms_spec.rb index 5df67ab..1ae650f 100644 --- a/spec/typesense/synonyms_spec.rb +++ b/spec/typesense/synonyms_spec.rb @@ -19,6 +19,10 @@ } end + before do + skip('Synonyms is deprecated in Typesense v30+, use SynonymSets instead') if typesense_v30_or_above? + end + describe '#upsert' do it 'creates an synonym rule and returns it' do stub_request(:put, Typesense::ApiCall.new(typesense.configuration).send(:uri_for, '/collections/companies/synonyms/synonym-set-1', typesense.configuration.nodes[0]))