Skip to content

Commit

Permalink
Controls: Add DiscoveryEngine::ControlClient
Browse files Browse the repository at this point in the history
This adds a client class for the Discovery Engine API, able to
synchronise a local `Control` instance.

It also adds a custom error class for this client and other future
clients, and some basic logic to try and figure out if a particular
error is caused by Discovery Engine disliking a filter expression, in
which case we can attach the error to the correct field.
  • Loading branch information
csutter committed Feb 3, 2025
1 parent ef774ba commit 5686e3b
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 0 deletions.
2 changes: 2 additions & 0 deletions app/clients/client_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# A generic error that occurred during a client operation
class ClientError < StandardError; end
58 changes: 58 additions & 0 deletions app/clients/discovery_engine/control_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
module DiscoveryEngine
# Client to synchronise `Control`s to Discovery Engine
class ControlClient
# Creates a corresponding resource for this control on Discovery Engine.
def create(control)
discovery_engine_client.create_control(
control: control.to_discovery_engine_control,
control_id: control.discovery_engine_id,
parent: control.parent,
)
rescue Google::Cloud::Error => e
set_record_errors(control, e)
raise ClientError, "Could not create control: #{e.message}"
end

# Updates the corresponding resource for this control on Discovery Engine.
def update(control)
discovery_engine_client.update_control(control: control.to_discovery_engine_control)
rescue Google::Cloud::Error => e
set_record_errors(control, e)
raise ClientError, "Could not update control: #{e.message}"
end

# Deletes the corresponding resource for this control on Discovery Engine.
def delete(control)
discovery_engine_client.delete_control(name: control.name)
rescue Google::Cloud::Error => e
set_record_errors(control, e)
raise ClientError, "Could not delete control: #{e.message}"
end

private

attr_reader :control

def set_record_errors(control, error)
# There is no way to extract structured error information from the Google API client, so we
# have to resort to regex matching to see if we can extract the cause of the error.
#
# In this case, we know that if the error message contains "filter syntax", the user probably
# made a mistake entering the filter expression and we can attach the error to that field.
# Otherwise, we consider it an unknown error and make sure to log it.
case error.message
when /filter syntax/i
control.action.errors.add(:filter_expression, error.details)
else
control.errors.add(:base, :remote_error)

GovukError.notify(error)
Rails.logger.error(error.message)
end
end

def discovery_engine_client
Google::Cloud::DiscoveryEngine.control_service(version: :v1)
end
end
end
101 changes: 101 additions & 0 deletions spec/clients/discovery_engine/control_client_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
RSpec.describe DiscoveryEngine::ControlClient, type: :client do
let(:control) { build(:control, id: 42) }

let(:discovery_engine_client) do
instance_double(
Google::Cloud::DiscoveryEngine::V1::ControlService::Client,
create_control: true,
update_control: true,
delete_control: true,
)
end

before do
allow(Google::Cloud::DiscoveryEngine)
.to receive(:control_service).and_return(discovery_engine_client)
end

describe "#create" do
it "creates the control on Discovery Engine" do
expect(discovery_engine_client).to receive(:create_control).with(
control: control.to_discovery_engine_control,
control_id: "search-admin-42",
parent: "[engine]",
)

subject.create(control) # rubocop:disable Rails/SaveBang (not an ActiveRecord model)
end
end

describe "#update" do
it "updates the control on Discovery Engine" do
expect(discovery_engine_client).to receive(:update_control).with(
control: control.to_discovery_engine_control,
)

subject.update(control) # rubocop:disable Rails/SaveBang (not an ActiveRecord model)
end

context "when the operation raises an arbitrary error" do
before do
allow(discovery_engine_client).to receive(:update_control).and_raise(Google::Cloud::Error)
end

it "raises a ClientInternalError and adds a base validation error" do
expect { subject.update(control) }.to raise_error(ClientError)

expect(control.errors).to be_of_kind(:base, :remote_error)
end
end

context "when the operation raises an invalid argument error about filter expressions" do
before do
allow(discovery_engine_client)
.to receive(:update_control)
.and_raise(Google::Cloud::InvalidArgumentError, "The filter syntax is broken")
end

it "raises a ClientInternalError and adds a field validation error on action" do
expect { subject.update(control) }.to raise_error(ClientError)

expect(control.action.errors).to be_of_kind(:filter_expression, :invalid)
end
end

context "when the operation raises an invalid argument error about anything else" do
before do
allow(discovery_engine_client)
.to receive(:update_control)
.and_raise(Google::Cloud::InvalidArgumentError, "The splines are unreticulated")
end

it "raises a ClientInternalError and adds a base validation error" do
expect { subject.update(control) }.to raise_error(ClientError)

expect(control.errors).to be_of_kind(:base, :remote_error)
end
end
end

describe "#delete" do
it "deletes the control on Discovery Engine" do
expect(discovery_engine_client).to receive(:delete_control).with(
name: "[engine]/controls/search-admin-42",
)

subject.delete(control)
end

context "when the operation raises an arbitrary error" do
before do
allow(discovery_engine_client).to receive(:delete_control).and_raise(Google::Cloud::Error)
end

it "raises a ClientInternalError and adds a base validation error" do
expect { subject.delete(control) }.to raise_error(ClientError)

expect(control.errors).to be_of_kind(:base, :remote_error)
end
end
end
end

0 comments on commit 5686e3b

Please sign in to comment.