-
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
RemoteSynchronizable
concern
This adds a `RemoteSynchronizable` model concern used by `Control` (and other models in the future) to persist themselves to DiscoveryEngine via the `DiscoveryEngine::Control` client. This concern uses ActiveRecord lifecycle callbacks to create/update/ delete a corresponding Control resource in Discovery Engine whenever its local version changes. Normally we would avoid using ActiveRecord callbacks to make network calls to remote APIs, but as the core purpose of Search Admin is to provide an interface to manage these resources on their various remote APIs, this is part of its core domain.
- Loading branch information
Showing
8 changed files
with
190 additions
and
1 deletion.
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
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,68 @@ | ||
# Enhances a model with lifecycle callbacks to synchronise it with a remote resource using a client | ||
# class (conventionally located in `app/clients/`). | ||
# | ||
# Example: | ||
# ```ruby | ||
# class Foo < ApplicationRecord | ||
# include RemoteSynchronizable | ||
# remote_synchronize with: BarApi::FooClient | ||
# end | ||
# ``` | ||
# | ||
# If the remote operation fails, the record will not be created/updated/destroyed, and will be | ||
# marked invalid. | ||
module RemoteSynchronizable | ||
extend ActiveSupport::Concern | ||
|
||
included do | ||
# Client class to use for synchronisation | ||
class_attribute :client_class | ||
|
||
# Create and update the remote resource using the client during ActiveRecord lifecycle events. | ||
# | ||
# Normally we would avoid using ActiveRecord callbacks to make network calls, but as the core | ||
# purpose of Search Admin is to provide an interface to manage resources on various remote APIs, | ||
# this is part of its core domain. | ||
before_create :create_remote, unless: :skip_remote_synchronization_on_create | ||
before_update :update_remote | ||
before_destroy :destroy_remote | ||
|
||
# Skips the creation of the remote synchronisation on create. | ||
# | ||
# This allows to create new instances of a record without a remote counterpart, for example | ||
# when importing existing remote resources, or as part of test setup (see `spec/factories.rb`). | ||
attr_accessor :skip_remote_synchronization_on_create | ||
end | ||
|
||
class_methods do | ||
# Set the class to be used for synchronisation for this model. It must allow initialisation with | ||
# a record, and respond to `#create`, `#update` and `#delete`. | ||
def remote_synchronize(with:) | ||
self.client_class = with | ||
end | ||
end | ||
|
||
private | ||
|
||
def create_remote | ||
client.create(self) # rubocop:disable Rails/SaveBang (not an ActiveRecord model) | ||
rescue ClientError | ||
raise ActiveRecord::RecordInvalid, self | ||
end | ||
|
||
def update_remote | ||
client.update(self) # rubocop:disable Rails/SaveBang (not an ActiveRecord model) | ||
rescue ClientError | ||
raise ActiveRecord::RecordInvalid, self | ||
end | ||
|
||
def destroy_remote | ||
client.delete(self) | ||
rescue ClientError | ||
throw :abort | ||
end | ||
|
||
def client | ||
@client ||= client_class.new | ||
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
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
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
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
83 changes: 83 additions & 0 deletions
83
spec/support/shared_examples/concerns/remote_synchronizable.rb
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,83 @@ | ||
RSpec.shared_examples "RemoteSynchronizable" do |client_class| | ||
let(:client) do | ||
instance_double(client_class, create: true, update: true, delete: true) | ||
end | ||
let(:factory) { described_class.model_name.param_key } | ||
|
||
before do | ||
allow(client_class).to receive(:new).and_return(client) | ||
end | ||
|
||
describe "when creating a new record" do | ||
subject(:record) { build(factory, skip_remote_synchronization_on_create: false) } | ||
|
||
it "creates the remote resource using the client" do | ||
record.save! | ||
|
||
expect(client).to have_received(:create).with(record) | ||
end | ||
|
||
context "when the remote resource creation fails" do | ||
let(:error) { ClientError.new("Uh oh") } | ||
|
||
before do | ||
allow(client).to receive(:create).and_raise(error) | ||
|
||
record.save # rubocop:disable Rails/SaveBang (we're checking record state) | ||
end | ||
|
||
it "stops the record from being created" do | ||
expect(record).not_to be_persisted | ||
end | ||
end | ||
end | ||
|
||
describe "when updating an existing record" do | ||
subject(:record) { create(factory) } | ||
|
||
it "updates the remote resource" do | ||
record.save! | ||
|
||
expect(client).to have_received(:update).with(record) | ||
end | ||
|
||
context "when the remote resource update fails" do | ||
let(:error) { ClientError.new("Uh oh") } | ||
|
||
before do | ||
allow(client).to receive(:update).and_raise(error) | ||
|
||
record.updated_at = Time.current # change something so we can check it won't save | ||
record.save # rubocop:disable Rails/SaveBang (we're checking record state) | ||
end | ||
|
||
it "stops the record from being persisted" do | ||
expect(record).to be_changed | ||
end | ||
end | ||
end | ||
|
||
describe "when destroying an existing record" do | ||
subject(:record) { create(factory) } | ||
|
||
it "deletes the remote resource" do | ||
record.destroy! | ||
|
||
expect(client).to have_received(:delete).with(record) | ||
end | ||
|
||
context "when the remote resource deletion fails with an internal error" do | ||
let(:error) { ClientError.new("Uh oh") } | ||
|
||
before do | ||
allow(client).to receive(:delete).and_raise(error) | ||
|
||
record.destroy # rubocop:disable Rails/SaveBang (we're checking record state) | ||
end | ||
|
||
it "stops the record from being destroyed" do | ||
expect(described_class.exists?(record.id)).to be(true) | ||
end | ||
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