-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Controls: Add
DiscoveryEngine::ControlClient
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
Showing
3 changed files
with
161 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |