From 431d6f4319f46a700e0fa884b83e40b39016d644 Mon Sep 17 00:00:00 2001 From: Sergio Bobillier Date: Thu, 12 Feb 2026 15:55:31 +0100 Subject: [PATCH 1/4] [JAY-737] Add the Indices::Settings class The class represents the settings for an Elasticsearch Index. It will be used in an upcoming commit to add a #settings method to the Elasticsearch::Index class, which will allow the client to retrieve the index's settings. --- CHANGELOG.md | 2 + lib/jay_api/elasticsearch.rb | 1 + lib/jay_api/elasticsearch/indices.rb | 10 ++ lib/jay_api/elasticsearch/indices/settings.rb | 43 ++++++ .../elasticsearch/indices/settings_spec.rb | 140 ++++++++++++++++++ 5 files changed, 196 insertions(+) create mode 100644 lib/jay_api/elasticsearch/indices.rb create mode 100644 lib/jay_api/elasticsearch/indices/settings.rb create mode 100644 spec/jay_api/elasticsearch/indices/settings_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index a4825b8..be9a0e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ Please mark backwards incompatible changes with an exclamation mark at the start when `active_support/core_ext/string` hadn't been loaded. ### Added +- The `Elasticsearch::Indices::Settings` class. The class encapsulates an + index's settings. - It is now possible to configure the type used by the `RSpec::TestDataCollector` class when pushing documents to Elasticsearch. If no type is specified in the configuration the default type will be used. diff --git a/lib/jay_api/elasticsearch.rb b/lib/jay_api/elasticsearch.rb index f4d233e..22588f3 100644 --- a/lib/jay_api/elasticsearch.rb +++ b/lib/jay_api/elasticsearch.rb @@ -7,6 +7,7 @@ require_relative 'elasticsearch/errors' require_relative 'elasticsearch/index' require_relative 'elasticsearch/indexes' +require_relative 'elasticsearch/indices' require_relative 'elasticsearch/query_builder' require_relative 'elasticsearch/query_results' require_relative 'elasticsearch/response' diff --git a/lib/jay_api/elasticsearch/indices.rb b/lib/jay_api/elasticsearch/indices.rb new file mode 100644 index 0000000..fcb5cb8 --- /dev/null +++ b/lib/jay_api/elasticsearch/indices.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require_relative 'indices/settings' + +module JayAPI + module Elasticsearch + # Namespace for sub-elements of Elasticsearch's indices + module Indices; end + end +end diff --git a/lib/jay_api/elasticsearch/indices/settings.rb b/lib/jay_api/elasticsearch/indices/settings.rb new file mode 100644 index 0000000..9bebe2d --- /dev/null +++ b/lib/jay_api/elasticsearch/indices/settings.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module JayAPI + module Elasticsearch + module Indices + # Represents the settings of an Elasticsearch Index. + class Settings + attr_reader :transport_client, :index_name + + # @param [Elasticsearch::Transport::Client] transport_client Elasticsearch's + # transport client. + # @param [String] index_name The name of the index this class will be + # handling settings for. + def initialize(transport_client, index_name) + @transport_client = transport_client + @index_name = index_name + end + + # @return [Hash] A Hash with all the settings for the index. It looks + # like this: + # + # { + # "number_of_shards" => "5", + # "blocks" => { "read_only_allow_delete" => "false", "write" => "false" }, + # "provided_name" => "xyz01_tests", + # "creation_date" => "1588701800423", + # "number_of_replicas" => "1", + # "uuid" => "VFx2e5t0Qgi-1zc2PUkYEg", + # "version" => { "created" => "7010199", "upgraded" => "7100299"} + # } + # + # @raise [Elasticsearch::Transport::Transport::Errors::ServerError] If + # an error occurs when trying to get the index's settings. + # @raise [KeyError] If any of the expected hierarchical elements in the + # response are missing. + def all + transport_client.indices.get_settings(index: index_name) + .fetch(index_name).fetch('settings').fetch('index') + end + end + end + end +end diff --git a/spec/jay_api/elasticsearch/indices/settings_spec.rb b/spec/jay_api/elasticsearch/indices/settings_spec.rb new file mode 100644 index 0000000..23978a8 --- /dev/null +++ b/spec/jay_api/elasticsearch/indices/settings_spec.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +require 'jay_api/elasticsearch/indices/settings' + +RSpec.describe JayAPI::Elasticsearch::Indices::Settings do + subject(:settings) { described_class.new(transport_client, index_name) } + + let(:indices_client) do + instance_double( + Elasticsearch::API::Indices::IndicesClient + ) + end + + let(:transport_client) do + instance_double( + Elasticsearch::Transport::Client, + indices: indices_client + ) + end + + let(:index_name) { 'xyz01_tests' } + + describe '#all' do + subject(:method_call) { settings.all } + + let(:index_settings) do + { + 'xyz01_tests' => { + 'settings' => { + 'index' => { + 'number_of_shards' => '5', + 'blocks' => { 'read_only_allow_delete' => 'false', 'write' => 'false' }, + 'provided_name' => 'xyz01_tests', + 'creation_date' => '1588701800423', + 'number_of_replicas' => '1', + 'uuid' => 'VFx2e5t0Qgi-1zc2PUkYEg', + 'version' => { 'created' => '7010199', 'upgraded' => '7100299' } + } + } + } + } + end + + let(:expected_hash) do + { + 'number_of_shards' => '5', + 'blocks' => { 'read_only_allow_delete' => 'false', 'write' => 'false' }, + 'provided_name' => 'xyz01_tests', + 'creation_date' => '1588701800423', + 'number_of_replicas' => '1', + 'uuid' => 'VFx2e5t0Qgi-1zc2PUkYEg', + 'version' => { 'created' => '7010199', 'upgraded' => '7100299' } + } + end + + before do + allow(indices_client).to receive(:get_settings) + .with(index: index_name).and_return(index_settings) + end + + it "fetches the index's settings using the transport client" do + expect(transport_client).to receive(:indices) + expect(indices_client).to receive(:get_settings).with(index: index_name) + method_call + end + + it 'returns the expected hash' do + expect(method_call).to eq(expected_hash) + end + + context 'when an HTTP error occurs' do + let(:error) do + [ + Elasticsearch::Transport::Transport::Errors::Unauthorized, + '401 - Unauthorized' + ] + end + + before do + allow(indices_client).to receive(:get_settings).and_raise(*error) + end + + it 're-raises the error' do + expect { method_call }.to raise_error(*error) + end + end + + context "when the response doesn't contain the setting for the expected index" do + let(:index_settings) do + { + 'xyz01_requirements' => { # <== Different index + 'settings' => { + 'index' => { + 'routing' => { 'allocation' => { 'initial_recovery' => { '_id' => nil } } }, + 'number_of_shards' => '5', + 'routing_partition_size' => '1', + 'blocks' => { 'read_only_allow_delete' => 'false', 'write' => 'false' }, + 'provided_name' => 'xyz01_requirements', + 'creation_date' => '1770731215226', + 'number_of_replicas' => '1', + 'uuid' => 'M00JT4urRsSPRytAWObGCA', + 'version' => { 'created' => '135248027', 'upgraded' => '135248027' } + } + } + } + } + end + + it 'raises a KeyError' do + expect { method_call }.to raise_error(KeyError, 'key not found: "xyz01_tests"') + end + end + + context "when the response doesn't contain the settings" do + let(:index_settings) do + { + 'xyz01_tests' => {} + } + end + + it 'raises a KeyError' do + expect { method_call }.to raise_error(KeyError, 'key not found: "settings"') + end + end + + context "when the response doesn't contain the index's settings" do + let(:index_settings) do + { + 'xyz01_tests' => { + 'settings' => {} + } + } + end + + it 'raises a KeyError' do + expect { method_call }.to raise_error(KeyError, 'key not found: "index"') + end + end + end +end From b1e9b4958a489b596574a4345cb430165eed4c8c Mon Sep 17 00:00:00 2001 From: Sergio Bobillier Date: Thu, 12 Feb 2026 18:21:50 +0100 Subject: [PATCH 2/4] [JAY-737] Add the Indices::Settings::Blocks class The class represents the blocks settings of an Elasticsearch index. For the moment the class only deals with the write block. This class will be used in an upcoming commit to complement the Indices::Settings class to allow the user to access and change an index's blocks settings. --- CHANGELOG.md | 2 + .../elasticsearch/indices/settings/blocks.rb | 59 +++++ .../indices/settings/blocks_spec.rb | 230 ++++++++++++++++++ 3 files changed, 291 insertions(+) create mode 100644 lib/jay_api/elasticsearch/indices/settings/blocks.rb create mode 100644 spec/jay_api/elasticsearch/indices/settings/blocks_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index be9a0e0..0b71a30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ Please mark backwards incompatible changes with an exclamation mark at the start when `active_support/core_ext/string` hadn't been loaded. ### Added +- The `Elasticsearch::Indices::Settings::Blocks` class. The class encapsulates + an index's blocks settings (for example, whether the index is read-only). - The `Elasticsearch::Indices::Settings` class. The class encapsulates an index's settings. - It is now possible to configure the type used by the `RSpec::TestDataCollector` diff --git a/lib/jay_api/elasticsearch/indices/settings/blocks.rb b/lib/jay_api/elasticsearch/indices/settings/blocks.rb new file mode 100644 index 0000000..1a49f48 --- /dev/null +++ b/lib/jay_api/elasticsearch/indices/settings/blocks.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module JayAPI + module Elasticsearch + module Indices + class Settings + # Represents the block settings of an Elasticsearch index. + class Blocks + attr_reader :settings + + # @param [JayAPI::Elasticsearch::Indices::Settings] settings The + # parent +Settings+ object. + def initialize(settings) + @settings = settings + end + + # @return [Boolean] True if the Index's write block has been set to + # true, false otherwise. + def write_blocked? + blocks_settings.fetch('write') == 'true' + end + + # Sets the index's +write+ block to the given value. When the +write+ + # block is set to +true+ the index's data is read-only, but the + # index's settings can still be changed. This allows maintenance tasks + # to still be performed on the index. + # @param [Boolean] value The new value for the +write+ block of the + # index. + # @raise [Elasticsearch::Transport::Transport::Errors::ServerError] If + # an error occurs when trying to set the value of the block. + def write=(value) + unless [true, false].include?(value) + raise ArgumentError, "Expected 'value' to be true or false, #{value.class} given" + end + + return if write_blocked? == value + + settings.transport_client.indices.put_settings( + index: settings.index_name, + body: { 'blocks.write' => value } + ) + end + + private + + # @return [Hash] The block settings of the index. Something like this: + # + # { 'read_only_allow_delete' => 'false', 'write' => 'false' } + # + # @raise [KeyError] If the index's settings do not contain a "blocks" + # section. + def blocks_settings + settings.all.fetch('blocks') + end + end + end + end + end +end diff --git a/spec/jay_api/elasticsearch/indices/settings/blocks_spec.rb b/spec/jay_api/elasticsearch/indices/settings/blocks_spec.rb new file mode 100644 index 0000000..081a862 --- /dev/null +++ b/spec/jay_api/elasticsearch/indices/settings/blocks_spec.rb @@ -0,0 +1,230 @@ +# frozen_string_literal: true + +require 'jay_api/elasticsearch/indices/settings' +require 'jay_api/elasticsearch/indices/settings/blocks' + +RSpec.describe JayAPI::Elasticsearch::Indices::Settings::Blocks do + subject(:blocks) { described_class.new(settings) } + + let(:blocks_settings) do + { 'read_only_allow_delete' => 'false', 'write' => 'false' } + end + + let(:index_settings) do + { + 'number_of_shards' => '5', + 'blocks' => blocks_settings, + 'provided_name' => 'xyz01_tests', + 'creation_date' => '1588701800423', + 'number_of_replicas' => '1', + 'uuid' => 'VFx2e5t0Qgi-1zc2PUkYEg', + 'version' => { 'created' => '7010199', 'upgraded' => '7100299' } + } + end + + let(:settings) do + instance_double( + JayAPI::Elasticsearch::Indices::Settings, + all: index_settings, + index_name: 'xyz01_tests' + ) + end + + shared_examples_for '#blocks_settings' do + it 'grabs the settings from the parent Settings object' do + expect(settings).to receive(:all) + method_call + end + + context 'when fetching the settings causes an HTTP error to be raised' do + let(:error) do + [ + Elasticsearch::Transport::Transport::Errors::NotFound, + '404 - Not Found' + ] + end + + before do + allow(settings).to receive(:all).and_raise(*error) + end + + it 're-raises the error' do + expect { method_call }.to raise_error(*error) + end + end + + context 'when fetching the settings causes an KeyError to be raised' do + let(:error) { [KeyError, 'key not found: "xyz01_tests"'] } + + before do + allow(settings).to receive(:all).and_raise(*error) + end + + it 're-raises the error' do + expect { method_call }.to raise_error(*error) + end + end + + context 'when the returned settings do not contain the "blocks" key' do + let(:index_settings) do + super().except('blocks') + end + + it 'raises a KeyError' do + expect { method_call }.to raise_error( + KeyError, 'key not found: "blocks"' + ) + end + end + end + + describe '#read_only?' do + subject(:method_call) { blocks.write_blocked? } + + it_behaves_like '#blocks_settings' + + context 'when the "write" block is active' do + let(:blocks_settings) { super().merge('write' => 'true') } + + it 'returns true' do + expect(method_call).to be(true) + end + end + + context 'when the "write" block is inactive' do + let(:blocks_settings) { super().merge('write' => 'false') } + + it 'returns false' do + expect(method_call).to be(false) + end + end + + context "when the response doesn't specify the status of the 'write' block" do + let(:blocks_settings) { super().except('write') } + + it 'raises a KeyError' do + expect { method_call }.to raise_error( + KeyError, 'key not found: "write"' + ) + end + end + end + + shared_examples_for '#read_only=' do + it 'takes the transport client from the parent Settings object' do + expect(settings).to receive(:transport_client) + method_call + end + + it "uses the parent Settings object's transport client to set the settings" do + expect(transport_client).to receive(:indices) + expect(indices_client).to receive(:put_settings) + method_call + end + + context "when updating the index's settings raises an HTTP error" do + let(:error) do + [ + Elasticsearch::Transport::Transport::Errors::RequestTimeout, + '408 - Timed out' + ] + end + + before do + allow(indices_client).to receive(:put_settings).and_raise(*error) + end + + it 're-raises the error' do + expect { method_call }.to raise_error(*error) + end + end + + it_behaves_like '#blocks_settings' + end + + describe '#read_only=' do + subject(:method_call) { blocks.write = value } + + let(:indices_client) do + instance_double( + Elasticsearch::API::Indices::IndicesClient, + put_settings: true + ) + end + + let(:transport_client) do + instance_double( + Elasticsearch::Transport::Client, + indices: indices_client + ) + end + + before do + allow(settings).to receive(:transport_client).and_return(transport_client) + end + + context "when 'value' is not a Boolean" do + let(:value) { 'true' } + + it 'raises an ArgumentError' do + expect { method_call }.to raise_error( + ArgumentError, "Expected 'value' to be true or false, String given" + ) + end + end + + context 'when the value is set to true' do + let(:value) { true } + + context 'when the index is already read-only' do + let(:blocks_settings) { super().merge('write' => 'true') } + + it "does not try to set the index's settings" do + expect(indices_client).not_to receive(:put_settings) + method_call + end + end + + context 'when the index is not already read-only' do + let(:blocks_settings) { super().merge('write' => 'false') } + + it_behaves_like '#read_only=' + + it 'sets the expected index settings' do + expect(indices_client).to receive(:put_settings).with( + index: 'xyz01_tests', body: { 'blocks.write' => true } + ) + + method_call + end + end + end + + context 'when the value is set to false' do + let(:value) { false } + + context 'when the index is read-only' do + let(:blocks_settings) { super().merge('write' => 'true') } + + it_behaves_like '#read_only=' + + it 'sets the expected index settings' do + expect(indices_client).to receive(:put_settings).with( + index: 'xyz01_tests', body: { 'blocks.write' => false } + ) + + method_call + end + end + + context 'when the index is not read-only' do + let(:blocks_settings) { super().merge('write' => 'false') } + + it "does not try to set the index's settings" do + expect(indices_client).not_to receive(:put_settings) + method_call + end + end + end + end +end From b823e24da35185d91600325c3bd088e110ee1d16 Mon Sep 17 00:00:00 2001 From: Sergio Bobillier Date: Fri, 13 Feb 2026 11:10:24 +0100 Subject: [PATCH 3/4] [JAY-737] Add the Indices::Settings#blocks method The method gives the caller access to the index's blocks settings. --- lib/jay_api/elasticsearch/indices/settings.rb | 8 +++++++ .../elasticsearch/indices/settings_spec.rb | 23 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/lib/jay_api/elasticsearch/indices/settings.rb b/lib/jay_api/elasticsearch/indices/settings.rb index 9bebe2d..231e596 100644 --- a/lib/jay_api/elasticsearch/indices/settings.rb +++ b/lib/jay_api/elasticsearch/indices/settings.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative 'settings/blocks' + module JayAPI module Elasticsearch module Indices @@ -37,6 +39,12 @@ def all transport_client.indices.get_settings(index: index_name) .fetch(index_name).fetch('settings').fetch('index') end + + # @return [JayAPI::Elasticsearch::Indices::Settings::Blocks] The blocks + # settings for the given index. + def blocks + @blocks ||= ::JayAPI::Elasticsearch::Indices::Settings::Blocks.new(self) + end end end end diff --git a/spec/jay_api/elasticsearch/indices/settings_spec.rb b/spec/jay_api/elasticsearch/indices/settings_spec.rb index 23978a8..bb15c06 100644 --- a/spec/jay_api/elasticsearch/indices/settings_spec.rb +++ b/spec/jay_api/elasticsearch/indices/settings_spec.rb @@ -137,4 +137,27 @@ end end end + + describe '#blocks' do + subject(:method_call) { settings.blocks } + + let(:blocks) do + instance_double( + JayAPI::Elasticsearch::Indices::Settings::Blocks + ) + end + + before do + allow(JayAPI::Elasticsearch::Indices::Settings::Blocks).to receive(:new).and_return(blocks) + end + + it 'creates an instance of the Blocks class with the expected parameters' do + expect(JayAPI::Elasticsearch::Indices::Settings::Blocks).to receive(:new).with(settings) + method_call + end + + it 'returns the instance of the Blocks class' do + expect(method_call).to be(blocks) + end + end end From 2879886d2506d6830ec21415b21cb26cdb8f5be3 Mon Sep 17 00:00:00 2001 From: Sergio Bobillier Date: Fri, 13 Feb 2026 10:45:10 +0100 Subject: [PATCH 4/4] [JAY-737] Add the Index#settings method The method returns an instance of the Elasticsearch::Indices::Settings class, which gives the caller access to the index's settings. --- CHANGELOG.md | 2 ++ lib/jay_api/elasticsearch/index.rb | 8 ++++++ spec/jay_api/elasticsearch/index_spec.rb | 26 +++++++++++++++++++ .../jay_api/elasticsearch/indexable_shared.rb | 9 ++++++- 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b71a30..b53adcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ Please mark backwards incompatible changes with an exclamation mark at the start when `active_support/core_ext/string` hadn't been loaded. ### Added +- The `#settings` method to the `Elasticsearch::Index` class. This gives the + caller access to the index's settings. - The `Elasticsearch::Indices::Settings::Blocks` class. The class encapsulates an index's blocks settings (for example, whether the index is read-only). - The `Elasticsearch::Indices::Settings` class. The class encapsulates an diff --git a/lib/jay_api/elasticsearch/index.rb b/lib/jay_api/elasticsearch/index.rb index 4ca1336..0999228 100644 --- a/lib/jay_api/elasticsearch/index.rb +++ b/lib/jay_api/elasticsearch/index.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative 'indexable' +require_relative 'indices/settings' module JayAPI module Elasticsearch @@ -49,6 +50,13 @@ def index_name def index(data, type: DEFAULT_DOC_TYPE) super.first end + + # @return [JayAPI::Elasticsearch::Indices::Settings] The settings for the + # index. + def settings + # DO NOT MEMOIZE! Leave it to the caller. + ::JayAPI::Elasticsearch::Indices::Settings.new(client.transport_client, index_name) + end end end end diff --git a/spec/jay_api/elasticsearch/index_spec.rb b/spec/jay_api/elasticsearch/index_spec.rb index 8c3dd7e..d39cf52 100644 --- a/spec/jay_api/elasticsearch/index_spec.rb +++ b/spec/jay_api/elasticsearch/index_spec.rb @@ -205,6 +205,32 @@ it_behaves_like 'Indexable#validate_type' end + describe '#settings' do + subject(:method_call) { index.settings } + + let(:settings) do + instance_double( + JayAPI::Elasticsearch::Indices::Settings + ) + end + + before do + allow(JayAPI::Elasticsearch::Indices::Settings) + .to receive(:new).and_return(settings) + end + + it 'creates an instance of the Settings class with the expected parameters' do + expect(JayAPI::Elasticsearch::Indices::Settings) + .to receive(:new).with(transport_client, 'elite_unit_tests') + + method_call + end + + it 'returns the Settings object' do + expect(method_call).to be(settings) + end + end + describe '#queue_size' do subject(:method_call) { index.queue_size } diff --git a/spec/jay_api/elasticsearch/indexable_shared.rb b/spec/jay_api/elasticsearch/indexable_shared.rb index 8384078..4649e27 100644 --- a/spec/jay_api/elasticsearch/indexable_shared.rb +++ b/spec/jay_api/elasticsearch/indexable_shared.rb @@ -3,11 +3,18 @@ RSpec.shared_context 'with mocked objects for Elasticsearch::Indexable' do let(:response) { {} } + let(:transport_client) do + instance_double( + Elasticsearch::Transport::Client + ) + end + let(:client) do instance_double( JayAPI::Elasticsearch::Client, bulk: successful_response, - search: response + search: response, + transport_client: ) end