From 1749119f6f6d7ff4118b6042e3f1f0dd08192b25 Mon Sep 17 00:00:00 2001 From: "Marcos G. Zimmermann" Date: Wed, 10 Jul 2024 12:25:05 -0300 Subject: [PATCH 01/15] chore: use candidate release of esse --- Gemfile | 4 +++- Gemfile.lock | 16 +++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index d89959c..736b545 100644 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,9 @@ source 'https://rubygems.org' -gem 'esse', '~> 0.2.4' +git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } + +gem 'esse', github: 'marcosgz/esse', branch: 'main' gem 'sqlite3', '~> 1.7.3' gem 'activerecord', '~> 5.2' gem 'esse-rspec', '~> 0.0.6' diff --git a/Gemfile.lock b/Gemfile.lock index cc50584..ff8eec1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,12 @@ +GIT + remote: https://github.com/marcosgz/esse + revision: 9d9e0fdf92fc72f834b9dd50bf242878f088a23d + branch: main + specs: + esse (0.3.0) + multi_json + thor (>= 0.19) + PATH remote: . specs: @@ -39,9 +48,6 @@ GEM elasticsearch-transport (7.17.10) faraday (>= 1, < 3) multi_json - esse (0.2.6) - multi_json - thor (>= 0.19) esse-rspec (0.0.6) esse (>= 0.2.4) rspec (>= 3) @@ -122,7 +128,7 @@ GEM standard-performance (1.0.1) lint_roller (~> 1.0) rubocop-performance (~> 1.16.0) - thor (1.3.0) + thor (1.3.1) thread_safe (0.3.6) tzinfo (1.2.11) thread_safe (~> 0.1) @@ -142,7 +148,7 @@ DEPENDENCIES awesome_print dotenv elasticsearch (~> 7.17, >= 7.17.10) - esse (~> 0.2.4) + esse! esse-active_record! esse-rspec (~> 0.0.6) pry From 2c337b26cb60f7fec4099a9d1bcdfa8736539a93 Mon Sep 17 00:00:00 2001 From: "Marcos G. Zimmermann" Date: Wed, 10 Jul 2024 13:04:21 -0300 Subject: [PATCH 02/15] chore: rename index_callbacks to index_callback --- README.md | 6 ++-- lib/esse/active_record/hooks.rb | 2 +- lib/esse/active_record/model.rb | 3 +- spec/esse/active_record/model_spec.rb | 48 +++++++++++++-------------- 4 files changed, 30 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 76d2656..6deca27 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ end ### Indexing Callbacks -The `index_callbacks` callback can be used to automaitcally index or delete documents after commit on create/update/destroy events. +The `index_callback` callback can be used to automaitcally index or delete documents after commit on create/update/destroy events. ```ruby class UsersIndex < Esse::Index @@ -173,9 +173,9 @@ class User < ApplicationRecord # Using a index and repository as argument. Note that the index name is used instead of the # of the constant name. it's so because index and model depends on each other should result in # circular dependencies issues. - index_callbacks 'users_index:user' + index_callback 'users_index:user' # Using a block to direct a different object to be indexed - index_callbacks('organizations') { user.organization } # The `_index` suffix and repo name is optional on the index name + index_callback('organizations') { user.organization } # The `_index` suffix and repo name is optional on the index name end ``` diff --git a/lib/esse/active_record/hooks.rb b/lib/esse/active_record/hooks.rb index 1b0e124..6aba262 100644 --- a/lib/esse/active_record/hooks.rb +++ b/lib/esse/active_record/hooks.rb @@ -82,7 +82,7 @@ def disable_model!(model_class, *repos) def ensure_registered_model_class!(model_class) return if registered_model_class?(model_class) - raise ArgumentError, "Model class #{model_class} is not registered. The model should inherit from Esse::ActiveRecord::Model and have a `index_callbacks' callback defined" + raise ArgumentError, "Model class #{model_class} is not registered. The model should inherit from Esse::ActiveRecord::Model and have a `index_callback' callback defined" end # Check if the given model is enabled for indexing. If no repository is specified, all repositories will be checked. diff --git a/lib/esse/active_record/model.rb b/lib/esse/active_record/model.rb index eae263c..c7269b9 100644 --- a/lib/esse/active_record/model.rb +++ b/lib/esse/active_record/model.rb @@ -21,7 +21,7 @@ module ClassMethods # For namespace, use `/` as the separator. # @raise [ArgumentError] when the repo and events are already registered # @raise [ArgumentError] when the specified index have multiple repos - def index_callbacks(index_repo_name, on: %i[create update destroy], **options, &block) + def index_callback(index_repo_name, on: %i[create update destroy], **options, &block) @esse_index_repos ||= {} operation_name = :index @@ -91,6 +91,7 @@ def index_callbacks(index_repo_name, on: %i[create update destroy], **options, & end end end + alias_method :index_callbacks, :index_callback # backward compatibility # Disable indexing for the block execution on model level # Example: diff --git a/spec/esse/active_record/model_spec.rb b/spec/esse/active_record/model_spec.rb index ca69b70..fd47492 100644 --- a/spec/esse/active_record/model_spec.rb +++ b/spec/esse/active_record/model_spec.rb @@ -51,14 +51,14 @@ clean_db end - describe '.index_callbacks' do + describe '.index_callback' do context 'when on :create' do let(:index_ok_response) { { 'result' => 'indexed' } } it 'register the model class into Esse::ActiveRecord::Hooks.models' do model_class = Class.new(State) do include Esse::ActiveRecord::Model - index_callbacks 'states_index', on: %i[create] + index_callback 'states_index', on: %i[create] end expect(Esse::ActiveRecord::Hooks.models).to include(model_class) end @@ -66,7 +66,7 @@ it 'index the model on create' do model_class = Class.new(State) do include Esse::ActiveRecord::Model - index_callbacks 'states_index', on: %i[create] + index_callback 'states_index', on: %i[create] end model = build_record(model_class, name: 'Illinois', id: SecureRandom.uuid) @@ -83,7 +83,7 @@ it 'index the associated model using the block definition' do model_class = Class.new(County) do include Esse::ActiveRecord::Model - index_callbacks 'geographies:state', on: %i[create] do + index_callback 'geographies:state', on: %i[create] do state end end @@ -103,7 +103,7 @@ it 'does not index when the hooks are globally disabled' do model_class = Class.new(State) do include Esse::ActiveRecord::Model - index_callbacks 'geographies:state', on: %i[create] + index_callback 'geographies:state', on: %i[create] end model = build_record(model_class, name: 'Illinois', id: SecureRandom.uuid) @@ -116,7 +116,7 @@ it 'does not index when the hooks are disabled for the model' do model_class = Class.new(State) do include Esse::ActiveRecord::Model - index_callbacks 'geographies:state', on: %i[create] + index_callback 'geographies:state', on: %i[create] end model = build_record(model_class, name: 'Illinois', id: SecureRandom.uuid) expect(GeographiesIndex).not_to receive(:index) @@ -128,8 +128,8 @@ it 'allows to select which indices will not execute indexing callbacks' do model_class = Class.new(State) do include Esse::ActiveRecord::Model - index_callbacks 'states', on: %i[create] - index_callbacks 'geographies:state', on: %i[create] + index_callback 'states', on: %i[create] + index_callback 'geographies:state', on: %i[create] end model = build_record(model_class, name: 'Illinois', id: SecureRandom.uuid) expect(GeographiesIndex).not_to receive(:index) @@ -151,7 +151,7 @@ it 'register the model class into Esse::ActiveRecord::Hooks.models' do model_class = Class.new(State) do include Esse::ActiveRecord::Model - index_callbacks 'states_index', on: %i[update] + index_callback 'states_index', on: %i[update] end expect(Esse::ActiveRecord::Hooks.models).to include(model_class) end @@ -159,7 +159,7 @@ it 'index the model on update' do model_class = Class.new(State) do include Esse::ActiveRecord::Model - index_callbacks 'states_index', on: %i[update] + index_callback 'states_index', on: %i[update] end model = create_record(model_class, name: 'Illinois') @@ -176,7 +176,7 @@ it 'index the associated model using the block definition' do model_class = Class.new(County) do include Esse::ActiveRecord::Model - index_callbacks 'geographies:state', on: %i[update] do + index_callback 'geographies:state', on: %i[update] do state end end @@ -196,7 +196,7 @@ it 'does not index when the hooks are globally disabled' do model_class = Class.new(State) do include Esse::ActiveRecord::Model - index_callbacks 'geographies:state', on: %i[update] + index_callback 'geographies:state', on: %i[update] end model = create_record(model_class, name: 'Illinois') @@ -209,7 +209,7 @@ it 'does not index when the hooks are disabled for the model' do model_class = Class.new(State) do include Esse::ActiveRecord::Model - index_callbacks 'geographies:state', on: %i[update] + index_callback 'geographies:state', on: %i[update] end model = create_record(model_class, name: 'Illinois') expect(GeographiesIndex).not_to receive(:index) @@ -221,8 +221,8 @@ it 'allows to select which indices will not execute indexing callbacks' do model_class = Class.new(State) do include Esse::ActiveRecord::Model - index_callbacks 'states', on: %i[update] - index_callbacks 'geographies:state', on: %i[update] + index_callback 'states', on: %i[update] + index_callback 'geographies:state', on: %i[update] end model = create_record(model_class, name: 'Illinois') expect(GeographiesIndex).not_to receive(:index) @@ -243,7 +243,7 @@ let(:model_class) do Class.new(County) do include Esse::ActiveRecord::Model - index_callbacks 'geographies:county', on: %i[update] + index_callback 'geographies:county', on: %i[update] end end let(:il) { create_record(State, name: 'Illinois') } @@ -309,7 +309,7 @@ it 'register the model class into Esse::ActiveRecord::Hooks.models' do model_class = Class.new(State) do include Esse::ActiveRecord::Model - index_callbacks 'states', on: %i[destroy] + index_callback 'states', on: %i[destroy] end expect(Esse::ActiveRecord::Hooks.models).to include(model_class) end @@ -317,7 +317,7 @@ it 'removes the document on destroy' do model_class = Class.new(State) do include Esse::ActiveRecord::Model - index_callbacks 'states', on: %i[destroy] + index_callback 'states', on: %i[destroy] end model = create_record(model_class, name: 'Illinois') expect(StatesIndex).to receive(:delete).and_call_original @@ -330,7 +330,7 @@ it 'does not raise error when the document does not exist' do model_class = Class.new(State) do include Esse::ActiveRecord::Model - index_callbacks 'geographies:state', on: %i[destroy] + index_callback 'geographies:state', on: %i[destroy] end model = create_record(model_class, name: 'Illinois') expect(GeographiesIndex).to receive(:delete).and_call_original @@ -343,7 +343,7 @@ it 'removes the associated model using the block definition' do model_class = Class.new(County) do include Esse::ActiveRecord::Model - index_callbacks 'geographies:state', on: %i[destroy] do + index_callback 'geographies:state', on: %i[destroy] do state end end @@ -359,7 +359,7 @@ it 'does not perform delete request when the hooks are globally disabled' do model_class = Class.new(State) do include Esse::ActiveRecord::Model - index_callbacks 'geographies:state', on: %i[destroy] + index_callback 'geographies:state', on: %i[destroy] end model = create_record(model_class, name: 'Illinois') @@ -372,7 +372,7 @@ it 'does not perform delete request when the hooks are disabled for the model' do model_class = Class.new(State) do include Esse::ActiveRecord::Model - index_callbacks 'geographies:state', on: %i[destroy] + index_callback 'geographies:state', on: %i[destroy] end model = create_record(model_class, name: 'Illinois') @@ -385,8 +385,8 @@ it 'allows to select which indices will NOT perform :delete request during callbacks' do model_class = Class.new(State) do include Esse::ActiveRecord::Model - index_callbacks 'states:state', on: %i[destroy] - index_callbacks 'geographies:state', on: %i[destroy] + index_callback 'states:state', on: %i[destroy] + index_callback 'geographies:state', on: %i[destroy] end model = create_record(model_class, name: 'Illinois') From 91450c5a7175c01901ead90723f16bec2dc22fc9 Mon Sep 17 00:00:00 2001 From: "Marcos G. Zimmermann" Date: Wed, 10 Jul 2024 13:16:38 -0300 Subject: [PATCH 03/15] chore: rename esse_index_repos to esse_callbacks --- .../active_record/callbacks/after_create.rb | 0 lib/esse/active_record/hooks.rb | 2 +- lib/esse/active_record/model.rb | 22 +++++++++++-------- spec/esse/active_record/hooks_spec.rb | 4 ++-- 4 files changed, 16 insertions(+), 12 deletions(-) create mode 100644 lib/esse/active_record/callbacks/after_create.rb diff --git a/lib/esse/active_record/callbacks/after_create.rb b/lib/esse/active_record/callbacks/after_create.rb new file mode 100644 index 0000000..e69de29 diff --git a/lib/esse/active_record/hooks.rb b/lib/esse/active_record/hooks.rb index 6aba262..7f9fb58 100644 --- a/lib/esse/active_record/hooks.rb +++ b/lib/esse/active_record/hooks.rb @@ -147,7 +147,7 @@ def all_repos # Returns a list of all repositories for the given model # @return [Array] def model_repos(model_class) - expand_index_repos(*model_class.esse_index_repos.keys) + expand_index_repos(*model_class.esse_callbacks.keys) end # Returns a list of all repositories for the given model diff --git a/lib/esse/active_record/model.rb b/lib/esse/active_record/model.rb index c7269b9..a2ef766 100644 --- a/lib/esse/active_record/model.rb +++ b/lib/esse/active_record/model.rb @@ -7,11 +7,15 @@ module Model def self.inherited(subclass) super - subclass.esse_index_repos = esse_index_repos.dup + subclass.instance_variable_set(:@esse_callbacks, esse_callbacks.dup) end module ClassMethods - attr_reader :esse_index_repos + attr_reader :esse_callbacks + + def esse_index_repos + esse_callbacks + end # Define callback for create/update/delete elasticsearch index document after model commit. # @@ -22,15 +26,15 @@ module ClassMethods # @raise [ArgumentError] when the repo and events are already registered # @raise [ArgumentError] when the specified index have multiple repos def index_callback(index_repo_name, on: %i[create update destroy], **options, &block) - @esse_index_repos ||= {} + @esse_callbacks ||= {} operation_name = :index - if esse_index_repos.dig(index_repo_name, operation_name) + if esse_callbacks.dig(index_repo_name, operation_name) raise ArgumentError, format('index repository %p already registered %s operation', name: index_repo_name, op: operation_name) end - esse_index_repos[index_repo_name] ||= {} - esse_index_repos[index_repo_name][operation_name] = { + esse_callbacks[index_repo_name] ||= {} + esse_callbacks[index_repo_name][operation_name] = { record: block || -> { self }, options: options, } @@ -40,7 +44,7 @@ def index_callback(index_repo_name, on: %i[create update destroy], **options, &b if_enabled = -> { Esse::ActiveRecord::Hooks.enabled?(index_repo_name) && Esse::ActiveRecord::Hooks.enabled_for_model?(self.class, index_repo_name) } (on & %i[create]).each do |event| after_commit(on: event, if: if_enabled) do - opts = self.class.esse_index_repos.fetch(index_repo_name).fetch(operation_name) + opts = self.class.esse_callbacks.fetch(index_repo_name).fetch(operation_name) record = opts.fetch(:record) record = instance_exec(&record) if record.respond_to?(:call) repo = Esse::ActiveRecord::Hooks.resolve_index_repository(index_repo_name) @@ -51,7 +55,7 @@ def index_callback(index_repo_name, on: %i[create update destroy], **options, &b end (on & %i[update]).each do |event| after_commit(on: event, if: if_enabled) do - opts = self.class.esse_index_repos.fetch(index_repo_name).fetch(operation_name) + opts = self.class.esse_callbacks.fetch(index_repo_name).fetch(operation_name) record = opts.fetch(:record) record = instance_exec(&record) if record.respond_to?(:call) repo = Esse::ActiveRecord::Hooks.resolve_index_repository(index_repo_name) @@ -79,7 +83,7 @@ def index_callback(index_repo_name, on: %i[create update destroy], **options, &b end (on & %i[destroy]).each do |event| after_commit(on: event, if: if_enabled) do - opts = self.class.esse_index_repos.fetch(index_repo_name).fetch(operation_name) + opts = self.class.esse_callbacks.fetch(index_repo_name).fetch(operation_name) record = opts.fetch(:record) record = instance_exec(&record) if record.respond_to?(:call) repo = Esse::ActiveRecord::Hooks.resolve_index_repository(index_repo_name) diff --git a/spec/esse/active_record/hooks_spec.rb b/spec/esse/active_record/hooks_spec.rb index a105768..720e16d 100644 --- a/spec/esse/active_record/hooks_spec.rb +++ b/spec/esse/active_record/hooks_spec.rb @@ -6,7 +6,7 @@ include Esse::ActiveRecord::Model Esse::ActiveRecord::Hooks.register_model(self) - instance_variable_set(:@esse_index_repos, { + instance_variable_set(:@esse_callbacks, { AnimalsIndex::Cat => {}, AnimalsIndex::Dog => {}, }) @@ -18,7 +18,7 @@ include Esse::ActiveRecord::Model Esse::ActiveRecord::Hooks.register_model(self) - instance_variable_set(:@esse_index_repos, { + instance_variable_set(:@esse_callbacks, { UsersIndex::User => {}, }) end From f2c3e972ab9a7a39efec8b76c4e5a2957647d168 Mon Sep 17 00:00:00 2001 From: "Marcos G. Zimmermann" Date: Wed, 10 Jul 2024 13:18:50 -0300 Subject: [PATCH 04/15] chore: deprecate renamed methods --- lib/esse/active_record/model.rb | 9 ++++++++- ...ecations_on_model_for_esse_index_repos_spec.rb | 15 +++++++++++++++ ...recations_on_model_for_index_callbacks_spec.rb | 15 +++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 spec/esse/active_record/deprecations_on_model_for_esse_index_repos_spec.rb create mode 100644 spec/esse/active_record/deprecations_on_model_for_index_callbacks_spec.rb diff --git a/lib/esse/active_record/model.rb b/lib/esse/active_record/model.rb index a2ef766..7baadc3 100644 --- a/lib/esse/active_record/model.rb +++ b/lib/esse/active_record/model.rb @@ -11,11 +11,14 @@ def self.inherited(subclass) end module ClassMethods + extend Esse::Deprecations::Deprecate + attr_reader :esse_callbacks def esse_index_repos esse_callbacks end + deprecate :esse_index_repos, :esse_callbacks, 2024, 12 # Define callback for create/update/delete elasticsearch index document after model commit. # @@ -95,7 +98,11 @@ def index_callback(index_repo_name, on: %i[create update destroy], **options, &b end end end - alias_method :index_callbacks, :index_callback # backward compatibility + + def index_callbacks(*args, **options, &block) + index_callback(*args, **options, &block) + end + deprecate :index_callbacks, :index_callback, 2024, 12 # Disable indexing for the block execution on model level # Example: diff --git a/spec/esse/active_record/deprecations_on_model_for_esse_index_repos_spec.rb b/spec/esse/active_record/deprecations_on_model_for_esse_index_repos_spec.rb new file mode 100644 index 0000000..7ff2307 --- /dev/null +++ b/spec/esse/active_record/deprecations_on_model_for_esse_index_repos_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Esse::ActiveRecord::Model, '.esse_index_repos' do + specify do + Gem::Deprecate.skip_during do + model_class = Class.new(State) do + include Esse::ActiveRecord::Model + index_callback 'states_index' + end + expect(model_class.esse_index_repos.keys).to include('states_index') + end + end +end diff --git a/spec/esse/active_record/deprecations_on_model_for_index_callbacks_spec.rb b/spec/esse/active_record/deprecations_on_model_for_index_callbacks_spec.rb new file mode 100644 index 0000000..b7722a4 --- /dev/null +++ b/spec/esse/active_record/deprecations_on_model_for_index_callbacks_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Esse::ActiveRecord::Model, '.index_callbacks' do + specify do + Gem::Deprecate.skip_during do + model_class = Class.new(State) do + include Esse::ActiveRecord::Model + index_callbacks 'states_index', on: %i[create] + end + expect(Esse::ActiveRecord::Hooks.models).to include(model_class) + end + end +end From 15d5f8abdf14bdb43c3f8cb2d523b1a3f40daebf Mon Sep 17 00:00:00 2001 From: "Marcos G. Zimmermann" Date: Wed, 10 Jul 2024 13:26:11 -0300 Subject: [PATCH 05/15] chore: group deprecated methods --- lib/esse/active_record/model.rb | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/esse/active_record/model.rb b/lib/esse/active_record/model.rb index 7baadc3..95bd142 100644 --- a/lib/esse/active_record/model.rb +++ b/lib/esse/active_record/model.rb @@ -15,11 +15,6 @@ module ClassMethods attr_reader :esse_callbacks - def esse_index_repos - esse_callbacks - end - deprecate :esse_index_repos, :esse_callbacks, 2024, 12 - # Define callback for create/update/delete elasticsearch index document after model commit. # # @param [String] index_repo_name The path of index and repository name. @@ -99,11 +94,6 @@ def index_callback(index_repo_name, on: %i[create update destroy], **options, &b end end - def index_callbacks(*args, **options, &block) - index_callback(*args, **options, &block) - end - deprecate :index_callbacks, :index_callback, 2024, 12 - # Disable indexing for the block execution on model level # Example: # User.without_indexing { } @@ -113,6 +103,16 @@ def without_indexing(*repos) yield end end + + def index_callbacks(*args, **options, &block) + index_callback(*args, **options, &block) + end + deprecate :index_callbacks, :index_callback, 2024, 12 + + def esse_index_repos + esse_callbacks + end + deprecate :esse_index_repos, :esse_callbacks, 2024, 12 end end end From e309e1337609de944b0a2e05b3bbc2c82bb7b21d Mon Sep 17 00:00:00 2001 From: "Marcos G. Zimmermann" Date: Wed, 10 Jul 2024 14:15:38 -0300 Subject: [PATCH 06/15] chore: expanding the existing callback to support different callback types --- lib/esse/active_record.rb | 1 + lib/esse/active_record/callbacks.rb | 55 +++++++++++++++++ .../active_record/callbacks/after_create.rb | 0 .../callbacks/indexing_on_create.rb | 10 +++ .../callbacks/indexing_on_destroy.rb | 10 +++ .../callbacks/indexing_on_update.rb | 10 +++ spec/esse/active_record/callbacks_spec.rb | 61 +++++++++++++++++++ 7 files changed, 147 insertions(+) create mode 100644 lib/esse/active_record/callbacks.rb delete mode 100644 lib/esse/active_record/callbacks/after_create.rb create mode 100644 lib/esse/active_record/callbacks/indexing_on_create.rb create mode 100644 lib/esse/active_record/callbacks/indexing_on_destroy.rb create mode 100644 lib/esse/active_record/callbacks/indexing_on_update.rb create mode 100644 spec/esse/active_record/callbacks_spec.rb diff --git a/lib/esse/active_record.rb b/lib/esse/active_record.rb index d45d81c..60288f2 100644 --- a/lib/esse/active_record.rb +++ b/lib/esse/active_record.rb @@ -3,6 +3,7 @@ require 'esse' require 'active_record' require_relative 'active_record/version' +require_relative 'active_record/callbacks' require_relative 'active_record/model' require_relative 'active_record/hooks' require_relative 'active_record/collection' diff --git a/lib/esse/active_record/callbacks.rb b/lib/esse/active_record/callbacks.rb new file mode 100644 index 0000000..ec325f2 --- /dev/null +++ b/lib/esse/active_record/callbacks.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Esse + module ActiveRecord + class Callback + attr_reader :options, :block + + def initialize(**kwargs, &block) + @options = kwargs + @block = block + end + + def call + raise NotImplementedError, 'You must implement #call method' + end + end + + module Callbacks + class << self + def to_h + @callbacks || {} + end + + def register_callback(identifier, operation, callback_class) + unless callback_class < Esse::ActiveRecord::Callback + raise ArgumentError, 'callback_class must be a subclass of Esse::ActiveRecord::Callback' + end + + key = :"#{identifier}_on_#{operation}" + + @callbacks ||= {} + if @callbacks.key?(key) + raise ArgumentError, "callback #{identifier} for #{operation} operation already registered" + end + + @callbacks[key] = callback_class + end + + def registered?(identifier, operation) + return false unless @callbacks + + @callbacks.key?(:"#{identifier}_on_#{operation}") + end + + def fetch(identifier, operation) + @callbacks[:"#{identifier}_on_#{operation}"] + end + end + end + end +end + +require_relative 'callbacks/indexing_on_create' +require_relative 'callbacks/indexing_on_update' +require_relative 'callbacks/indexing_on_destroy' diff --git a/lib/esse/active_record/callbacks/after_create.rb b/lib/esse/active_record/callbacks/after_create.rb deleted file mode 100644 index e69de29..0000000 diff --git a/lib/esse/active_record/callbacks/indexing_on_create.rb b/lib/esse/active_record/callbacks/indexing_on_create.rb new file mode 100644 index 0000000..a381d14 --- /dev/null +++ b/lib/esse/active_record/callbacks/indexing_on_create.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Esse::ActiveRecord + module Callbacks + class IndexingOnCreate < Callback + end + + register_callback(:index, :create, IndexingOnCreate) + end +end diff --git a/lib/esse/active_record/callbacks/indexing_on_destroy.rb b/lib/esse/active_record/callbacks/indexing_on_destroy.rb new file mode 100644 index 0000000..5f8f4c1 --- /dev/null +++ b/lib/esse/active_record/callbacks/indexing_on_destroy.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Esse::ActiveRecord + module Callbacks + class IndexingOnDestroy < Callback + end + + register_callback(:index, :destroy, IndexingOnDestroy) + end +end diff --git a/lib/esse/active_record/callbacks/indexing_on_update.rb b/lib/esse/active_record/callbacks/indexing_on_update.rb new file mode 100644 index 0000000..cb9ab7d --- /dev/null +++ b/lib/esse/active_record/callbacks/indexing_on_update.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Esse::ActiveRecord + module Callbacks + class IndexingOnUpdate < Callback + end + + register_callback(:index, :update, IndexingOnUpdate) + end +end diff --git a/spec/esse/active_record/callbacks_spec.rb b/spec/esse/active_record/callbacks_spec.rb new file mode 100644 index 0000000..990160e --- /dev/null +++ b/spec/esse/active_record/callbacks_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +RSpec.describe Esse::ActiveRecord::Callbacks do + before do + described_class.instance_variable_set(:@callbacks, nil) + end + + describe '.to_h' do + it 'returns an empty hash' do + expect(described_class.to_h).to eq({}) + end + end + + describe '.register_callback' do + it 'registers a callback' do + expect { + described_class.register_callback(:external, :create, Class.new(Esse::ActiveRecord::Callback)) + }.to change { described_class.to_h.size }.by(1) + end + + it 'raises an error if the callback is already registered' do + described_class.register_callback(:external, :create, Class.new(Esse::ActiveRecord::Callback)) + expect { + described_class.register_callback(:external, :create, Class.new(Esse::ActiveRecord::Callback)) + }.to raise_error(ArgumentError, 'callback external for create operation already registered') + end + end + + describe '.registered?' do + it 'returns false if the callback is not registered' do + expect(described_class.registered?(:external, :create)).to eq(false) + end + + it 'returns true if the callback is registered' do + described_class.register_callback(:external, :create, Class.new(Esse::ActiveRecord::Callback)) + expect(described_class.registered?(:external, :create)).to eq(true) + end + end +end + +RSpec.describe Esse::ActiveRecord::Callback do + let(:callback_class) do + Class.new(described_class) + end + + it 'raises an error if #call is not implemented' do + expect { + callback_class.new.call + }.to raise_error(NotImplementedError, 'You must implement #call method') + end + + it 'has options' do + callback = callback_class.new(foo: 'bar') + expect(callback.options).to eq(foo: 'bar') + end + + it 'has a block' do + callback = callback_class.new { 'foo' } + expect(callback.block.call).to eq('foo') + end +end From f2109e68f7987ecd2fe27cdde5dfa16fb25f11a6 Mon Sep 17 00:00:00 2001 From: "Marcos G. Zimmermann" Date: Wed, 10 Jul 2024 14:51:23 -0300 Subject: [PATCH 07/15] feat: refactoring indexing callback by moving them to a callback repository --- lib/esse/active_record/callbacks.rb | 18 ++- .../callbacks/indexing_on_create.rb | 6 + .../callbacks/indexing_on_destroy.rb | 8 ++ .../callbacks/indexing_on_update.rb | 24 ++++ lib/esse/active_record/model.rb | 106 ++++++------------ spec/esse/active_record/callbacks_spec.rb | 30 ++++- 6 files changed, 107 insertions(+), 85 deletions(-) diff --git a/lib/esse/active_record/callbacks.rb b/lib/esse/active_record/callbacks.rb index ec325f2..cf30d77 100644 --- a/lib/esse/active_record/callbacks.rb +++ b/lib/esse/active_record/callbacks.rb @@ -3,14 +3,15 @@ module Esse module ActiveRecord class Callback - attr_reader :options, :block + attr_reader :repo, :options, :block_result - def initialize(**kwargs, &block) + def initialize(repo:, block_result: nil, **kwargs) + @repo = repo @options = kwargs - @block = block + @block_result = block_result end - def call + def call(model) raise NotImplementedError, 'You must implement #call method' end end @@ -42,8 +43,13 @@ def registered?(identifier, operation) @callbacks.key?(:"#{identifier}_on_#{operation}") end - def fetch(identifier, operation) - @callbacks[:"#{identifier}_on_#{operation}"] + def fetch!(identifier, operation) + key = :"#{identifier}_on_#{operation}" + if registered?(identifier, operation) + [key, @callbacks[key]] + else + raise ArgumentError, "callback #{identifier} for #{operation} operation not registered" + end end end end diff --git a/lib/esse/active_record/callbacks/indexing_on_create.rb b/lib/esse/active_record/callbacks/indexing_on_create.rb index a381d14..0ecb27c 100644 --- a/lib/esse/active_record/callbacks/indexing_on_create.rb +++ b/lib/esse/active_record/callbacks/indexing_on_create.rb @@ -3,6 +3,12 @@ module Esse::ActiveRecord module Callbacks class IndexingOnCreate < Callback + def call(model) + record = block_result || model + document = repo.serialize(record) + repo.index.index(document, **options) if document + true + end end register_callback(:index, :create, IndexingOnCreate) diff --git a/lib/esse/active_record/callbacks/indexing_on_destroy.rb b/lib/esse/active_record/callbacks/indexing_on_destroy.rb index 5f8f4c1..b24bc4b 100644 --- a/lib/esse/active_record/callbacks/indexing_on_destroy.rb +++ b/lib/esse/active_record/callbacks/indexing_on_destroy.rb @@ -3,6 +3,14 @@ module Esse::ActiveRecord module Callbacks class IndexingOnDestroy < Callback + def call(model) + record = block_result || model + document = repo.serialize(record) + repo.index.delete(document, **options) if document + true + rescue Esse::Transport::NotFoundError + true + end end register_callback(:index, :destroy, IndexingOnDestroy) diff --git a/lib/esse/active_record/callbacks/indexing_on_update.rb b/lib/esse/active_record/callbacks/indexing_on_update.rb index cb9ab7d..bc20002 100644 --- a/lib/esse/active_record/callbacks/indexing_on_update.rb +++ b/lib/esse/active_record/callbacks/indexing_on_update.rb @@ -3,6 +3,30 @@ module Esse::ActiveRecord module Callbacks class IndexingOnUpdate < Callback + def call(model) + record = block_result || model + + document = repo.serialize(record) + return true unless document + + repo.index.index(document, **options) + return true unless document.routing + + prev_record = model.class.new(model.attributes.merge(model.previous_changes.transform_values(&:first))).tap(&:readonly!) + prev_document = repo.serialize(prev_record) + + return true unless prev_document + return true if [prev_document.id, prev_document.routing].include?(nil) + return true if prev_document.routing == document.routing + return true if prev_document.id != document.id + + begin + repo.index.delete(prev_document, **options) + rescue Esse::Transport::NotFoundError + end + + true + end end register_callback(:index, :update, IndexingOnUpdate) diff --git a/lib/esse/active_record/model.rb b/lib/esse/active_record/model.rb index 95bd142..3a94c19 100644 --- a/lib/esse/active_record/model.rb +++ b/lib/esse/active_record/model.rb @@ -5,93 +5,51 @@ module ActiveRecord module Model extend ActiveSupport::Concern - def self.inherited(subclass) - super - subclass.instance_variable_set(:@esse_callbacks, esse_callbacks.dup) - end - module ClassMethods extend Esse::Deprecations::Deprecate - attr_reader :esse_callbacks + def esse_callbacks + @esse_callbacks ||= {}.freeze + end - # Define callback for create/update/delete elasticsearch index document after model commit. - # - # @param [String] index_repo_name The path of index and repository name. - # For example a index with a single repository named `users` is `users`. And a index with - # multiple repositories named `animals` and `dog` as the repository name is `animals/dog`. - # For namespace, use `/` as the separator. - # @raise [ArgumentError] when the repo and events are already registered - # @raise [ArgumentError] when the specified index have multiple repos - def index_callback(index_repo_name, on: %i[create update destroy], **options, &block) - @esse_callbacks ||= {} + def esse_callback(index_repo_name, operation_name, on: %i[create update destroy], **options, &block) + @esse_callbacks = esse_callbacks.dup + if_enabled = -> { Esse::ActiveRecord::Hooks.enabled?(index_repo_name) && Esse::ActiveRecord::Hooks.enabled_for_model?(self.class, index_repo_name) } - operation_name = :index - if esse_callbacks.dig(index_repo_name, operation_name) - raise ArgumentError, format('index repository %p already registered %s operation', name: index_repo_name, op: operation_name) - end + Array(on).each do |event| + identifier, klass = Esse::ActiveRecord::Callbacks.fetch!(operation_name, event) - esse_callbacks[index_repo_name] ||= {} - esse_callbacks[index_repo_name][operation_name] = { - record: block || -> { self }, - options: options, - } + if @esse_callbacks.dig(index_repo_name, identifier) + raise ArgumentError, format('index repository %p already registered %s operation', name: index_repo_name, op: operation_name) + end - Esse::ActiveRecord::Hooks.register_model(self) + @esse_callbacks[index_repo_name] ||= {} + @esse_callbacks[index_repo_name][identifier] = [klass, options, block] - if_enabled = -> { Esse::ActiveRecord::Hooks.enabled?(index_repo_name) && Esse::ActiveRecord::Hooks.enabled_for_model?(self.class, index_repo_name) } - (on & %i[create]).each do |event| after_commit(on: event, if: if_enabled) do - opts = self.class.esse_callbacks.fetch(index_repo_name).fetch(operation_name) - record = opts.fetch(:record) - record = instance_exec(&record) if record.respond_to?(:call) - repo = Esse::ActiveRecord::Hooks.resolve_index_repository(index_repo_name) - document = repo.serialize(record) - repo.index.index(document, **opts[:options]) if document - true + klass, options, block = self.class.esse_callbacks.fetch(index_repo_name).fetch(identifier) + options[:repo] = Esse::ActiveRecord::Hooks.resolve_index_repository(index_repo_name) + options[:block_result] = instance_exec(&block) if block.respond_to?(:call) + instance = klass.new(**options) + instance.call(self) end end - (on & %i[update]).each do |event| - after_commit(on: event, if: if_enabled) do - opts = self.class.esse_callbacks.fetch(index_repo_name).fetch(operation_name) - record = opts.fetch(:record) - record = instance_exec(&record) if record.respond_to?(:call) - repo = Esse::ActiveRecord::Hooks.resolve_index_repository(index_repo_name) - document = repo.serialize(record) - next true unless document - - repo.index.index(document, **opts[:options]) - next true unless document.routing - prev_record = self.class.new(attributes.merge(previous_changes.transform_values(&:first))).tap(&:readonly!) - prev_document = repo.serialize(prev_record) - - next true unless prev_document - next true if [prev_document.id, prev_document.routing].include?(nil) - next true if prev_document.routing == document.routing - next true if prev_document.id != document.id - - begin - repo.index.delete(prev_document, **opts[:options]) - rescue Esse::Transport::NotFoundError - end + Esse::ActiveRecord::Hooks.register_model(self) + ensure + @esse_callbacks&.each_value { |v| v.freeze }&.freeze + end - true - end - end - (on & %i[destroy]).each do |event| - after_commit(on: event, if: if_enabled) do - opts = self.class.esse_callbacks.fetch(index_repo_name).fetch(operation_name) - record = opts.fetch(:record) - record = instance_exec(&record) if record.respond_to?(:call) - repo = Esse::ActiveRecord::Hooks.resolve_index_repository(index_repo_name) - document = repo.serialize(record) - repo.index.delete(document, **opts[:options]) if document - true - rescue Esse::Transport::NotFoundError - true - end - end + # Define callback for create/update/delete elasticsearch index document after model commit. + # + # @param [String] index_repo_name The path of index and repository name. + # For example a index with a single repository named `users` is `users`. And a index with + # multiple repositories named `animals` and `dog` as the repository name is `animals/dog`. + # For namespace, use `/` as the separator. + # @raise [ArgumentError] when the repo and events are already registered + # @raise [ArgumentError] when the specified index have multiple repos + def index_callback(index_repo_name, on: %i[create update destroy], **options, &block) + esse_callback(index_repo_name, :index, on: on, **options, &block) end # Disable indexing for the block execution on model level diff --git a/spec/esse/active_record/callbacks_spec.rb b/spec/esse/active_record/callbacks_spec.rb index 990160e..88286d2 100644 --- a/spec/esse/active_record/callbacks_spec.rb +++ b/spec/esse/active_record/callbacks_spec.rb @@ -2,9 +2,14 @@ RSpec.describe Esse::ActiveRecord::Callbacks do before do + @__callbacks = described_class.instance_variable_get(:@callbacks) described_class.instance_variable_set(:@callbacks, nil) end + after do + described_class.instance_variable_set(:@callbacks, @__callbacks) + end + describe '.to_h' do it 'returns an empty hash' do expect(described_class.to_h).to eq({}) @@ -36,26 +41,41 @@ expect(described_class.registered?(:external, :create)).to eq(true) end end + + describe '.fetch!' do + it 'raises an error if the callback is not registered' do + expect { + described_class.fetch!(:external, :create) + }.to raise_error(ArgumentError, 'callback external for create operation not registered') + end + + it 'returns the callback class' do + klass = Class.new(Esse::ActiveRecord::Callback) + described_class.register_callback(:external, :create, klass) + expect(described_class.fetch!(:external, :create)).to eq([:external_on_create, klass]) + end + end end RSpec.describe Esse::ActiveRecord::Callback do let(:callback_class) do Class.new(described_class) end + let(:repo) { double } it 'raises an error if #call is not implemented' do expect { - callback_class.new.call + callback_class.new(repo: repo).call(nil) }.to raise_error(NotImplementedError, 'You must implement #call method') end it 'has options' do - callback = callback_class.new(foo: 'bar') + callback = callback_class.new(repo: repo, foo: 'bar') expect(callback.options).to eq(foo: 'bar') end - it 'has a block' do - callback = callback_class.new { 'foo' } - expect(callback.block.call).to eq('foo') + it 'has a block result' do + callback = callback_class.new(repo: repo, block_result: 'result') + expect(callback.block_result).to eq('result') end end From 34e7b9a1543dfb1ae4414cd9048f3764b91bd8f0 Mon Sep 17 00:00:00 2001 From: "Marcos G. Zimmermann" Date: Wed, 10 Jul 2024 14:52:31 -0300 Subject: [PATCH 08/15] chore: renaming :index to :indexing --- lib/esse/active_record/callbacks/indexing_on_create.rb | 2 +- lib/esse/active_record/callbacks/indexing_on_destroy.rb | 2 +- lib/esse/active_record/callbacks/indexing_on_update.rb | 2 +- lib/esse/active_record/model.rb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/esse/active_record/callbacks/indexing_on_create.rb b/lib/esse/active_record/callbacks/indexing_on_create.rb index 0ecb27c..19b3a0c 100644 --- a/lib/esse/active_record/callbacks/indexing_on_create.rb +++ b/lib/esse/active_record/callbacks/indexing_on_create.rb @@ -11,6 +11,6 @@ def call(model) end end - register_callback(:index, :create, IndexingOnCreate) + register_callback(:indexing, :create, IndexingOnCreate) end end diff --git a/lib/esse/active_record/callbacks/indexing_on_destroy.rb b/lib/esse/active_record/callbacks/indexing_on_destroy.rb index b24bc4b..d092b9f 100644 --- a/lib/esse/active_record/callbacks/indexing_on_destroy.rb +++ b/lib/esse/active_record/callbacks/indexing_on_destroy.rb @@ -13,6 +13,6 @@ def call(model) end end - register_callback(:index, :destroy, IndexingOnDestroy) + register_callback(:indexing, :destroy, IndexingOnDestroy) end end diff --git a/lib/esse/active_record/callbacks/indexing_on_update.rb b/lib/esse/active_record/callbacks/indexing_on_update.rb index bc20002..ea280eb 100644 --- a/lib/esse/active_record/callbacks/indexing_on_update.rb +++ b/lib/esse/active_record/callbacks/indexing_on_update.rb @@ -29,6 +29,6 @@ def call(model) end end - register_callback(:index, :update, IndexingOnUpdate) + register_callback(:indexing, :update, IndexingOnUpdate) end end diff --git a/lib/esse/active_record/model.rb b/lib/esse/active_record/model.rb index 3a94c19..657d92a 100644 --- a/lib/esse/active_record/model.rb +++ b/lib/esse/active_record/model.rb @@ -49,7 +49,7 @@ def esse_callback(index_repo_name, operation_name, on: %i[create update destroy] # @raise [ArgumentError] when the repo and events are already registered # @raise [ArgumentError] when the specified index have multiple repos def index_callback(index_repo_name, on: %i[create update destroy], **options, &block) - esse_callback(index_repo_name, :index, on: on, **options, &block) + esse_callback(index_repo_name, :indexing, on: on, **options, &block) end # Disable indexing for the block execution on model level From 0bc0728aa4c660399d2981c0c05ce17f2df937cb Mon Sep 17 00:00:00 2001 From: "Marcos G. Zimmermann" Date: Wed, 10 Jul 2024 15:00:28 -0300 Subject: [PATCH 09/15] chore: refactoring specs --- lib/esse/active_record/model.rb | 4 + ...deprecations_for_esse_index_repos_spec.rb} | 0 .../deprecations_for_index_callbacks_spec.rb} | 0 .../model/index_callback_spec.rb | 405 ++++++++++++++++++ spec/esse/active_record/model_spec.rb | 402 +---------------- 5 files changed, 416 insertions(+), 395 deletions(-) rename spec/esse/active_record/{deprecations_on_model_for_esse_index_repos_spec.rb => model/deprecations_for_esse_index_repos_spec.rb} (100%) rename spec/esse/active_record/{deprecations_on_model_for_index_callbacks_spec.rb => model/deprecations_for_index_callbacks_spec.rb} (100%) create mode 100644 spec/esse/active_record/model/index_callback_spec.rb diff --git a/lib/esse/active_record/model.rb b/lib/esse/active_record/model.rb index 657d92a..73e03b7 100644 --- a/lib/esse/active_record/model.rb +++ b/lib/esse/active_record/model.rb @@ -52,6 +52,10 @@ def index_callback(index_repo_name, on: %i[create update destroy], **options, &b esse_callback(index_repo_name, :indexing, on: on, **options, &block) end + # def esse_update_lazy_document_attribute(index_repo_name, attribute_name, on: %i[create update destroy], **options, &block) + # esse_callback(index_repo_name, :update_lazy_attribute, on: on, **options, &block) + # end + # Disable indexing for the block execution on model level # Example: # User.without_indexing { } diff --git a/spec/esse/active_record/deprecations_on_model_for_esse_index_repos_spec.rb b/spec/esse/active_record/model/deprecations_for_esse_index_repos_spec.rb similarity index 100% rename from spec/esse/active_record/deprecations_on_model_for_esse_index_repos_spec.rb rename to spec/esse/active_record/model/deprecations_for_esse_index_repos_spec.rb diff --git a/spec/esse/active_record/deprecations_on_model_for_index_callbacks_spec.rb b/spec/esse/active_record/model/deprecations_for_index_callbacks_spec.rb similarity index 100% rename from spec/esse/active_record/deprecations_on_model_for_index_callbacks_spec.rb rename to spec/esse/active_record/model/deprecations_for_index_callbacks_spec.rb diff --git a/spec/esse/active_record/model/index_callback_spec.rb b/spec/esse/active_record/model/index_callback_spec.rb new file mode 100644 index 0000000..fd47492 --- /dev/null +++ b/spec/esse/active_record/model/index_callback_spec.rb @@ -0,0 +1,405 @@ +require 'spec_helper' + +RSpec.describe Esse::ActiveRecord::Model do + let(:backend_proxy) { double } + + before do + Thread.current[Esse::ActiveRecord::Hooks::STORE_STATE_KEY] = nil + @models_value_backup = Esse::ActiveRecord::Hooks.models.dup + Esse::ActiveRecord::Hooks.models.clear + stub_cluster_info + + stub_esse_index(:states) do + repository :state, const: true do + document do |state, **| + { + _id: state.id, + name: state.name, + } + end + end + end + + stub_esse_index(:geographies) do + repository :state, const: true do + document do |state, **| + { + _id: state.id, + name: state.name, + type: 'state', + } + end + end + + repository :county, const: true do + document do |county, **| + { + _id: county.id, + name: county.name, + type: 'county', + routing: county.state_id || 1, + }.tap do |doc| + doc[:state] = { id: county.state.id, name: county.state.name } if county.state + end + end + end + end + end + + after do + Esse::ActiveRecord::Hooks.instance_variable_set(:@models, @models_value_backup) # rubocop:disable RSpec/InstanceVariable + clean_db + end + + describe '.index_callback' do + context 'when on :create' do + let(:index_ok_response) { { 'result' => 'indexed' } } + + it 'register the model class into Esse::ActiveRecord::Hooks.models' do + model_class = Class.new(State) do + include Esse::ActiveRecord::Model + index_callback 'states_index', on: %i[create] + end + expect(Esse::ActiveRecord::Hooks.models).to include(model_class) + end + + it 'index the model on create' do + model_class = Class.new(State) do + include Esse::ActiveRecord::Model + index_callback 'states_index', on: %i[create] + end + model = build_record(model_class, name: 'Illinois', id: SecureRandom.uuid) + + expect(StatesIndex).to receive(:index).and_call_original + expect(StatesIndex).to esse_receive_request(:index).with( + id: model.id, + index: StatesIndex.index_name, + body: {name: 'Illinois'}, + ).and_return(index_ok_response) + + model.save + end + + it 'index the associated model using the block definition' do + model_class = Class.new(County) do + include Esse::ActiveRecord::Model + index_callback 'geographies:state', on: %i[create] do + state + end + end + state = build_record(State, name: 'Illinois', id: SecureRandom.uuid) + county = build_record(model_class, name: 'Cook', state: state) + + expect(GeographiesIndex).to receive(:index).and_call_original + expect(GeographiesIndex).to esse_receive_request(:index).with( + id: state.id, + index: GeographiesIndex.index_name, + body: {name: 'Illinois', type: 'state'}, + ).and_return(index_ok_response) + + county.save + end + + it 'does not index when the hooks are globally disabled' do + model_class = Class.new(State) do + include Esse::ActiveRecord::Model + index_callback 'geographies:state', on: %i[create] + end + model = build_record(model_class, name: 'Illinois', id: SecureRandom.uuid) + + expect(GeographiesIndex).not_to receive(:index) + Esse::ActiveRecord::Hooks.without_indexing do + model.save + end + end + + it 'does not index when the hooks are disabled for the model' do + model_class = Class.new(State) do + include Esse::ActiveRecord::Model + index_callback 'geographies:state', on: %i[create] + end + model = build_record(model_class, name: 'Illinois', id: SecureRandom.uuid) + expect(GeographiesIndex).not_to receive(:index) + model_class.without_indexing do + model.save + end + end + + it 'allows to select which indices will not execute indexing callbacks' do + model_class = Class.new(State) do + include Esse::ActiveRecord::Model + index_callback 'states', on: %i[create] + index_callback 'geographies:state', on: %i[create] + end + model = build_record(model_class, name: 'Illinois', id: SecureRandom.uuid) + expect(GeographiesIndex).not_to receive(:index) + expect(StatesIndex).to receive(:index).and_call_original + expect(StatesIndex).to esse_receive_request(:index).with( + id: model.id, + index: StatesIndex.index_name, + body: {name: 'Illinois'}, + ).and_return(index_ok_response) + model_class.without_indexing(GeographiesIndex) do + model.save + end + end + end + + context 'when on :update' do + let(:index_ok_response) { { 'result' => 'indexed' } } + + it 'register the model class into Esse::ActiveRecord::Hooks.models' do + model_class = Class.new(State) do + include Esse::ActiveRecord::Model + index_callback 'states_index', on: %i[update] + end + expect(Esse::ActiveRecord::Hooks.models).to include(model_class) + end + + it 'index the model on update' do + model_class = Class.new(State) do + include Esse::ActiveRecord::Model + index_callback 'states_index', on: %i[update] + end + model = create_record(model_class, name: 'Illinois') + + expect(StatesIndex).to receive(:index).and_call_original + expect(StatesIndex).to esse_receive_request(:index).with( + id: model.id, + index: StatesIndex.index_name, + body: {name: 'Illinois'}, + ).and_return(index_ok_response) + + model.touch + end + + it 'index the associated model using the block definition' do + model_class = Class.new(County) do + include Esse::ActiveRecord::Model + index_callback 'geographies:state', on: %i[update] do + state + end + end + state = create_record(State, name: 'Illinois') + county = create_record(model_class, name: 'Cook', state: state) + + expect(GeographiesIndex).to receive(:index).and_call_original + expect(GeographiesIndex).to esse_receive_request(:index).with( + id: state.id, + index: GeographiesIndex.index_name, + body: {name: 'Illinois', type: 'state'}, + ).and_return(index_ok_response) + + county.touch + end + + it 'does not index when the hooks are globally disabled' do + model_class = Class.new(State) do + include Esse::ActiveRecord::Model + index_callback 'geographies:state', on: %i[update] + end + model = create_record(model_class, name: 'Illinois') + + expect(GeographiesIndex).not_to receive(:index) + Esse::ActiveRecord::Hooks.without_indexing do + model.touch + end + end + + it 'does not index when the hooks are disabled for the model' do + model_class = Class.new(State) do + include Esse::ActiveRecord::Model + index_callback 'geographies:state', on: %i[update] + end + model = create_record(model_class, name: 'Illinois') + expect(GeographiesIndex).not_to receive(:index) + model_class.without_indexing do + model.touch + end + end + + it 'allows to select which indices will not execute indexing callbacks' do + model_class = Class.new(State) do + include Esse::ActiveRecord::Model + index_callback 'states', on: %i[update] + index_callback 'geographies:state', on: %i[update] + end + model = create_record(model_class, name: 'Illinois') + expect(GeographiesIndex).not_to receive(:index) + expect(StatesIndex).to receive(:index).and_call_original + expect(StatesIndex).to esse_receive_request(:index).with( + id: model.id, + index: StatesIndex.index_name, + body: {name: 'Illinois'}, + ).and_return(index_ok_response) + model_class.without_indexing(GeographiesIndex) do + model.touch + end + end + end + + context 'when on :update with a the document that has a routing key' do + let(:index_ok_response) { { 'result' => 'indexed' } } + let(:model_class) do + Class.new(County) do + include Esse::ActiveRecord::Model + index_callback 'geographies:county', on: %i[update] + end + end + let(:il) { create_record(State, name: 'Illinois') } + let(:ny) { create_record(State, name: 'New York') } + let(:county) { create_record(model_class, name: 'Cook', state: il) } + + it 'indexes the document in new routing and deletes the document from previous routing' do + expect(GeographiesIndex).to receive(:index).and_call_original + expect(GeographiesIndex).to esse_receive_request(:index).with( + id: county.id, + index: GeographiesIndex.index_name, + routing: ny.id, + body: {name: 'Cook', type: 'county', state: { id: ny.id, name: ny.name }}, + ).and_return(index_ok_response) + + expect(GeographiesIndex).to receive(:delete).and_call_original + expect(GeographiesIndex).to esse_receive_request(:delete).with( + id: county.id, + index: GeographiesIndex.index_name, + routing: il.id, + ).and_return('result' => 'deleted') + + county.update(state: ny) + end + + it 'does not delete the document when the routing key is not changed' do + expect(GeographiesIndex).to receive(:index).and_call_original + expect(GeographiesIndex).to esse_receive_request(:index).with( + id: county.id, + index: GeographiesIndex.index_name, + routing: il.id, + body: {name: 'Cook County', type: 'county', state: { id: il.id, name: il.name }}, + ).and_return(index_ok_response) + + expect(GeographiesIndex).not_to receive(:delete) + + county.update(name: 'Cook County') + end + + it 'does not raise error when the document does not exist' do + expect(GeographiesIndex).to receive(:index).and_call_original + expect(GeographiesIndex).to esse_receive_request(:index).with( + id: county.id, + index: GeographiesIndex.index_name, + routing: ny.id, + body: {name: 'Cook', type: 'county', state: { id: ny.id, name: ny.name }}, + ).and_return(index_ok_response) + + expect(GeographiesIndex).to receive(:delete).and_call_original + expect(GeographiesIndex).to esse_receive_request(:delete).with( + id: county.id, + index: GeographiesIndex.index_name, + routing: il.id, + ).and_raise_http_status(404, { 'error' => { 'type' => 'not_found' } }) + + county.update(state: ny) + end + end + + context 'when on destroy' do + let(:delete_ok_response) { { 'result' => 'deleted' } } + + it 'register the model class into Esse::ActiveRecord::Hooks.models' do + model_class = Class.new(State) do + include Esse::ActiveRecord::Model + index_callback 'states', on: %i[destroy] + end + expect(Esse::ActiveRecord::Hooks.models).to include(model_class) + end + + it 'removes the document on destroy' do + model_class = Class.new(State) do + include Esse::ActiveRecord::Model + index_callback 'states', on: %i[destroy] + end + model = create_record(model_class, name: 'Illinois') + expect(StatesIndex).to receive(:delete).and_call_original + expect(StatesIndex).to esse_receive_request(:delete).with( + id: model.id, + ).and_return(delete_ok_response) + model.destroy + end + + it 'does not raise error when the document does not exist' do + model_class = Class.new(State) do + include Esse::ActiveRecord::Model + index_callback 'geographies:state', on: %i[destroy] + end + model = create_record(model_class, name: 'Illinois') + expect(GeographiesIndex).to receive(:delete).and_call_original + expect(GeographiesIndex).to esse_receive_request(:delete).with( + id: model.id, + ).and_raise_http_status(404, { 'error' => { 'type' => 'not_found' } }) + expect { model.destroy }.not_to raise_error + end + + it 'removes the associated model using the block definition' do + model_class = Class.new(County) do + include Esse::ActiveRecord::Model + index_callback 'geographies:state', on: %i[destroy] do + state + end + end + state = create_record(State, name: 'Illinois') + county = create_record(model_class, name: 'Cook', state: state) + expect(GeographiesIndex).to receive(:delete).and_call_original + expect(GeographiesIndex).to esse_receive_request(:delete).with( + id: state.id, + ).and_return(delete_ok_response) + county.destroy + end + + it 'does not perform delete request when the hooks are globally disabled' do + model_class = Class.new(State) do + include Esse::ActiveRecord::Model + index_callback 'geographies:state', on: %i[destroy] + end + model = create_record(model_class, name: 'Illinois') + + expect(GeographiesIndex).not_to receive(:delete) + Esse::ActiveRecord::Hooks.without_indexing do + model.destroy + end + end + + it 'does not perform delete request when the hooks are disabled for the model' do + model_class = Class.new(State) do + include Esse::ActiveRecord::Model + index_callback 'geographies:state', on: %i[destroy] + end + model = create_record(model_class, name: 'Illinois') + + expect(GeographiesIndex).not_to receive(:delete) + model_class.without_indexing do + model.destroy + end + end + + it 'allows to select which indices will NOT perform :delete request during callbacks' do + model_class = Class.new(State) do + include Esse::ActiveRecord::Model + index_callback 'states:state', on: %i[destroy] + index_callback 'geographies:state', on: %i[destroy] + end + model = create_record(model_class, name: 'Illinois') + + expect(GeographiesIndex).not_to receive(:delete) + expect(StatesIndex).to receive(:delete).and_call_original + expect(StatesIndex).to esse_receive_request(:delete).with( + id: model.id, + ).and_return(delete_ok_response) + + model_class.without_indexing(GeographiesIndex) do + model.destroy + end + end + end + end +end diff --git a/spec/esse/active_record/model_spec.rb b/spec/esse/active_record/model_spec.rb index fd47492..164a00c 100644 --- a/spec/esse/active_record/model_spec.rb +++ b/spec/esse/active_record/model_spec.rb @@ -1,405 +1,17 @@ require 'spec_helper' RSpec.describe Esse::ActiveRecord::Model do - let(:backend_proxy) { double } - - before do - Thread.current[Esse::ActiveRecord::Hooks::STORE_STATE_KEY] = nil - @models_value_backup = Esse::ActiveRecord::Hooks.models.dup - Esse::ActiveRecord::Hooks.models.clear - stub_cluster_info - - stub_esse_index(:states) do - repository :state, const: true do - document do |state, **| - { - _id: state.id, - name: state.name, - } - end - end - end - - stub_esse_index(:geographies) do - repository :state, const: true do - document do |state, **| - { - _id: state.id, - name: state.name, - type: 'state', - } - end - end - - repository :county, const: true do - document do |county, **| - { - _id: county.id, - name: county.name, - type: 'county', - routing: county.state_id || 1, - }.tap do |doc| - doc[:state] = { id: county.state.id, name: county.state.name } if county.state - end - end - end + let(:model) do + Class.new(State) do + include Esse::ActiveRecord::Model end end - after do - Esse::ActiveRecord::Hooks.instance_variable_set(:@models, @models_value_backup) # rubocop:disable RSpec/InstanceVariable - clean_db + it 'responds to .index_callback' do + expect(model).to respond_to(:index_callback) end - describe '.index_callback' do - context 'when on :create' do - let(:index_ok_response) { { 'result' => 'indexed' } } - - it 'register the model class into Esse::ActiveRecord::Hooks.models' do - model_class = Class.new(State) do - include Esse::ActiveRecord::Model - index_callback 'states_index', on: %i[create] - end - expect(Esse::ActiveRecord::Hooks.models).to include(model_class) - end - - it 'index the model on create' do - model_class = Class.new(State) do - include Esse::ActiveRecord::Model - index_callback 'states_index', on: %i[create] - end - model = build_record(model_class, name: 'Illinois', id: SecureRandom.uuid) - - expect(StatesIndex).to receive(:index).and_call_original - expect(StatesIndex).to esse_receive_request(:index).with( - id: model.id, - index: StatesIndex.index_name, - body: {name: 'Illinois'}, - ).and_return(index_ok_response) - - model.save - end - - it 'index the associated model using the block definition' do - model_class = Class.new(County) do - include Esse::ActiveRecord::Model - index_callback 'geographies:state', on: %i[create] do - state - end - end - state = build_record(State, name: 'Illinois', id: SecureRandom.uuid) - county = build_record(model_class, name: 'Cook', state: state) - - expect(GeographiesIndex).to receive(:index).and_call_original - expect(GeographiesIndex).to esse_receive_request(:index).with( - id: state.id, - index: GeographiesIndex.index_name, - body: {name: 'Illinois', type: 'state'}, - ).and_return(index_ok_response) - - county.save - end - - it 'does not index when the hooks are globally disabled' do - model_class = Class.new(State) do - include Esse::ActiveRecord::Model - index_callback 'geographies:state', on: %i[create] - end - model = build_record(model_class, name: 'Illinois', id: SecureRandom.uuid) - - expect(GeographiesIndex).not_to receive(:index) - Esse::ActiveRecord::Hooks.without_indexing do - model.save - end - end - - it 'does not index when the hooks are disabled for the model' do - model_class = Class.new(State) do - include Esse::ActiveRecord::Model - index_callback 'geographies:state', on: %i[create] - end - model = build_record(model_class, name: 'Illinois', id: SecureRandom.uuid) - expect(GeographiesIndex).not_to receive(:index) - model_class.without_indexing do - model.save - end - end - - it 'allows to select which indices will not execute indexing callbacks' do - model_class = Class.new(State) do - include Esse::ActiveRecord::Model - index_callback 'states', on: %i[create] - index_callback 'geographies:state', on: %i[create] - end - model = build_record(model_class, name: 'Illinois', id: SecureRandom.uuid) - expect(GeographiesIndex).not_to receive(:index) - expect(StatesIndex).to receive(:index).and_call_original - expect(StatesIndex).to esse_receive_request(:index).with( - id: model.id, - index: StatesIndex.index_name, - body: {name: 'Illinois'}, - ).and_return(index_ok_response) - model_class.without_indexing(GeographiesIndex) do - model.save - end - end - end - - context 'when on :update' do - let(:index_ok_response) { { 'result' => 'indexed' } } - - it 'register the model class into Esse::ActiveRecord::Hooks.models' do - model_class = Class.new(State) do - include Esse::ActiveRecord::Model - index_callback 'states_index', on: %i[update] - end - expect(Esse::ActiveRecord::Hooks.models).to include(model_class) - end - - it 'index the model on update' do - model_class = Class.new(State) do - include Esse::ActiveRecord::Model - index_callback 'states_index', on: %i[update] - end - model = create_record(model_class, name: 'Illinois') - - expect(StatesIndex).to receive(:index).and_call_original - expect(StatesIndex).to esse_receive_request(:index).with( - id: model.id, - index: StatesIndex.index_name, - body: {name: 'Illinois'}, - ).and_return(index_ok_response) - - model.touch - end - - it 'index the associated model using the block definition' do - model_class = Class.new(County) do - include Esse::ActiveRecord::Model - index_callback 'geographies:state', on: %i[update] do - state - end - end - state = create_record(State, name: 'Illinois') - county = create_record(model_class, name: 'Cook', state: state) - - expect(GeographiesIndex).to receive(:index).and_call_original - expect(GeographiesIndex).to esse_receive_request(:index).with( - id: state.id, - index: GeographiesIndex.index_name, - body: {name: 'Illinois', type: 'state'}, - ).and_return(index_ok_response) - - county.touch - end - - it 'does not index when the hooks are globally disabled' do - model_class = Class.new(State) do - include Esse::ActiveRecord::Model - index_callback 'geographies:state', on: %i[update] - end - model = create_record(model_class, name: 'Illinois') - - expect(GeographiesIndex).not_to receive(:index) - Esse::ActiveRecord::Hooks.without_indexing do - model.touch - end - end - - it 'does not index when the hooks are disabled for the model' do - model_class = Class.new(State) do - include Esse::ActiveRecord::Model - index_callback 'geographies:state', on: %i[update] - end - model = create_record(model_class, name: 'Illinois') - expect(GeographiesIndex).not_to receive(:index) - model_class.without_indexing do - model.touch - end - end - - it 'allows to select which indices will not execute indexing callbacks' do - model_class = Class.new(State) do - include Esse::ActiveRecord::Model - index_callback 'states', on: %i[update] - index_callback 'geographies:state', on: %i[update] - end - model = create_record(model_class, name: 'Illinois') - expect(GeographiesIndex).not_to receive(:index) - expect(StatesIndex).to receive(:index).and_call_original - expect(StatesIndex).to esse_receive_request(:index).with( - id: model.id, - index: StatesIndex.index_name, - body: {name: 'Illinois'}, - ).and_return(index_ok_response) - model_class.without_indexing(GeographiesIndex) do - model.touch - end - end - end - - context 'when on :update with a the document that has a routing key' do - let(:index_ok_response) { { 'result' => 'indexed' } } - let(:model_class) do - Class.new(County) do - include Esse::ActiveRecord::Model - index_callback 'geographies:county', on: %i[update] - end - end - let(:il) { create_record(State, name: 'Illinois') } - let(:ny) { create_record(State, name: 'New York') } - let(:county) { create_record(model_class, name: 'Cook', state: il) } - - it 'indexes the document in new routing and deletes the document from previous routing' do - expect(GeographiesIndex).to receive(:index).and_call_original - expect(GeographiesIndex).to esse_receive_request(:index).with( - id: county.id, - index: GeographiesIndex.index_name, - routing: ny.id, - body: {name: 'Cook', type: 'county', state: { id: ny.id, name: ny.name }}, - ).and_return(index_ok_response) - - expect(GeographiesIndex).to receive(:delete).and_call_original - expect(GeographiesIndex).to esse_receive_request(:delete).with( - id: county.id, - index: GeographiesIndex.index_name, - routing: il.id, - ).and_return('result' => 'deleted') - - county.update(state: ny) - end - - it 'does not delete the document when the routing key is not changed' do - expect(GeographiesIndex).to receive(:index).and_call_original - expect(GeographiesIndex).to esse_receive_request(:index).with( - id: county.id, - index: GeographiesIndex.index_name, - routing: il.id, - body: {name: 'Cook County', type: 'county', state: { id: il.id, name: il.name }}, - ).and_return(index_ok_response) - - expect(GeographiesIndex).not_to receive(:delete) - - county.update(name: 'Cook County') - end - - it 'does not raise error when the document does not exist' do - expect(GeographiesIndex).to receive(:index).and_call_original - expect(GeographiesIndex).to esse_receive_request(:index).with( - id: county.id, - index: GeographiesIndex.index_name, - routing: ny.id, - body: {name: 'Cook', type: 'county', state: { id: ny.id, name: ny.name }}, - ).and_return(index_ok_response) - - expect(GeographiesIndex).to receive(:delete).and_call_original - expect(GeographiesIndex).to esse_receive_request(:delete).with( - id: county.id, - index: GeographiesIndex.index_name, - routing: il.id, - ).and_raise_http_status(404, { 'error' => { 'type' => 'not_found' } }) - - county.update(state: ny) - end - end - - context 'when on destroy' do - let(:delete_ok_response) { { 'result' => 'deleted' } } - - it 'register the model class into Esse::ActiveRecord::Hooks.models' do - model_class = Class.new(State) do - include Esse::ActiveRecord::Model - index_callback 'states', on: %i[destroy] - end - expect(Esse::ActiveRecord::Hooks.models).to include(model_class) - end - - it 'removes the document on destroy' do - model_class = Class.new(State) do - include Esse::ActiveRecord::Model - index_callback 'states', on: %i[destroy] - end - model = create_record(model_class, name: 'Illinois') - expect(StatesIndex).to receive(:delete).and_call_original - expect(StatesIndex).to esse_receive_request(:delete).with( - id: model.id, - ).and_return(delete_ok_response) - model.destroy - end - - it 'does not raise error when the document does not exist' do - model_class = Class.new(State) do - include Esse::ActiveRecord::Model - index_callback 'geographies:state', on: %i[destroy] - end - model = create_record(model_class, name: 'Illinois') - expect(GeographiesIndex).to receive(:delete).and_call_original - expect(GeographiesIndex).to esse_receive_request(:delete).with( - id: model.id, - ).and_raise_http_status(404, { 'error' => { 'type' => 'not_found' } }) - expect { model.destroy }.not_to raise_error - end - - it 'removes the associated model using the block definition' do - model_class = Class.new(County) do - include Esse::ActiveRecord::Model - index_callback 'geographies:state', on: %i[destroy] do - state - end - end - state = create_record(State, name: 'Illinois') - county = create_record(model_class, name: 'Cook', state: state) - expect(GeographiesIndex).to receive(:delete).and_call_original - expect(GeographiesIndex).to esse_receive_request(:delete).with( - id: state.id, - ).and_return(delete_ok_response) - county.destroy - end - - it 'does not perform delete request when the hooks are globally disabled' do - model_class = Class.new(State) do - include Esse::ActiveRecord::Model - index_callback 'geographies:state', on: %i[destroy] - end - model = create_record(model_class, name: 'Illinois') - - expect(GeographiesIndex).not_to receive(:delete) - Esse::ActiveRecord::Hooks.without_indexing do - model.destroy - end - end - - it 'does not perform delete request when the hooks are disabled for the model' do - model_class = Class.new(State) do - include Esse::ActiveRecord::Model - index_callback 'geographies:state', on: %i[destroy] - end - model = create_record(model_class, name: 'Illinois') - - expect(GeographiesIndex).not_to receive(:delete) - model_class.without_indexing do - model.destroy - end - end - - it 'allows to select which indices will NOT perform :delete request during callbacks' do - model_class = Class.new(State) do - include Esse::ActiveRecord::Model - index_callback 'states:state', on: %i[destroy] - index_callback 'geographies:state', on: %i[destroy] - end - model = create_record(model_class, name: 'Illinois') - - expect(GeographiesIndex).not_to receive(:delete) - expect(StatesIndex).to receive(:delete).and_call_original - expect(StatesIndex).to esse_receive_request(:delete).with( - id: model.id, - ).and_return(delete_ok_response) - - model_class.without_indexing(GeographiesIndex) do - model.destroy - end - end - end + it 'responds to .esse_callback' do + expect(model).to respond_to(:esse_callback) end end From cdf8f5576fe99465d064382161b0fc35b3028b32 Mon Sep 17 00:00:00 2001 From: "Marcos G. Zimmermann" Date: Wed, 10 Jul 2024 16:11:59 -0300 Subject: [PATCH 10/15] chore: add tests to the esse_callback --- README.md | 2 +- lib/esse/active_record/callbacks.rb | 7 +- .../callbacks/update_lazy_attribute.rb | 27 +++ lib/esse/active_record/model.rb | 7 +- spec/esse/active_record/callbacks_spec.rb | 8 +- .../active_record/model/esse_callback_spec.rb | 218 ++++++++++++++++++ .../model/index_callback_spec.rb | 2 +- 7 files changed, 263 insertions(+), 8 deletions(-) create mode 100644 lib/esse/active_record/callbacks/update_lazy_attribute.rb create mode 100644 spec/esse/active_record/model/esse_callback_spec.rb diff --git a/README.md b/README.md index 6deca27..4c4b835 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,7 @@ or by some specific list of index or index's repository ```ruby Esse::ActiveRecord::Hooks.disable!(UsersIndex.repo) Esse::ActiveRecord::Hooks.enable!(UsersIndex.repo) -Esse::ActiveRecord::Hooks.without_indexing(AccountsIndex UsersIndex.repo, ) do +Esse::ActiveRecord::Hooks.without_indexing(AccountsIndex, UsersIndex.repo) do 10.times { User.create! } end ``` diff --git a/lib/esse/active_record/callbacks.rb b/lib/esse/active_record/callbacks.rb index cf30d77..9890341 100644 --- a/lib/esse/active_record/callbacks.rb +++ b/lib/esse/active_record/callbacks.rb @@ -19,7 +19,7 @@ def call(model) module Callbacks class << self def to_h - @callbacks || {} + @callbacks || {}.freeze end def register_callback(identifier, operation, callback_class) @@ -29,12 +29,14 @@ def register_callback(identifier, operation, callback_class) key = :"#{identifier}_on_#{operation}" - @callbacks ||= {} + @callbacks = @callbacks ? @callbacks.dup : {} if @callbacks.key?(key) raise ArgumentError, "callback #{identifier} for #{operation} operation already registered" end @callbacks[key] = callback_class + ensure + @callbacks&.freeze end def registered?(identifier, operation) @@ -59,3 +61,4 @@ def fetch!(identifier, operation) require_relative 'callbacks/indexing_on_create' require_relative 'callbacks/indexing_on_update' require_relative 'callbacks/indexing_on_destroy' +require_relative 'callbacks/update_lazy_attribute' diff --git a/lib/esse/active_record/callbacks/update_lazy_attribute.rb b/lib/esse/active_record/callbacks/update_lazy_attribute.rb new file mode 100644 index 0000000..76ec0d1 --- /dev/null +++ b/lib/esse/active_record/callbacks/update_lazy_attribute.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Esse::ActiveRecord + module Callbacks + class UpdateLazyAttribute < Callback + attr_reader :attribute_name + + def initialize(attribute_name:, **kwargs, &block) + @attribute_name = attribute_name + super(**kwargs, &block) + end + + def call(model) + related_ids = Array(block_result || model.id) + return true if related_ids.empty? + + repo.update_documents_attribute(attribute_name, *related_ids, **options) + + true + end + end + + register_callback(:update_lazy_attribute, :create, UpdateLazyAttribute) + register_callback(:update_lazy_attribute, :update, UpdateLazyAttribute) + register_callback(:update_lazy_attribute, :destroy, UpdateLazyAttribute) + end +end diff --git a/lib/esse/active_record/model.rb b/lib/esse/active_record/model.rb index 73e03b7..b484f7c 100644 --- a/lib/esse/active_record/model.rb +++ b/lib/esse/active_record/model.rb @@ -52,9 +52,10 @@ def index_callback(index_repo_name, on: %i[create update destroy], **options, &b esse_callback(index_repo_name, :indexing, on: on, **options, &block) end - # def esse_update_lazy_document_attribute(index_repo_name, attribute_name, on: %i[create update destroy], **options, &block) - # esse_callback(index_repo_name, :update_lazy_attribute, on: on, **options, &block) - # end + def update_lazy_attribute_callback(index_repo_name, attribute_name, on: %i[create update destroy], **options, &block) + options[:attribute_name] = attribute_name + esse_callback(index_repo_name, :update_lazy_attribute, on: on, **options, &block) + end # Disable indexing for the block execution on model level # Example: diff --git a/spec/esse/active_record/callbacks_spec.rb b/spec/esse/active_record/callbacks_spec.rb index 88286d2..631973e 100644 --- a/spec/esse/active_record/callbacks_spec.rb +++ b/spec/esse/active_record/callbacks_spec.rb @@ -7,13 +7,17 @@ end after do - described_class.instance_variable_set(:@callbacks, @__callbacks) + described_class.instance_variable_set(:@callbacks, @__callbacks) # rubocop:disable RSpec/InstanceVariable end describe '.to_h' do it 'returns an empty hash' do expect(described_class.to_h).to eq({}) end + + it 'returns a frozen hash' do + expect(described_class.to_h).to be_frozen + end end describe '.register_callback' do @@ -21,6 +25,7 @@ expect { described_class.register_callback(:external, :create, Class.new(Esse::ActiveRecord::Callback)) }.to change { described_class.to_h.size }.by(1) + expect(described_class.to_h).to be_frozen end it 'raises an error if the callback is already registered' do @@ -28,6 +33,7 @@ expect { described_class.register_callback(:external, :create, Class.new(Esse::ActiveRecord::Callback)) }.to raise_error(ArgumentError, 'callback external for create operation already registered') + expect(described_class.to_h).to be_frozen end end diff --git a/spec/esse/active_record/model/esse_callback_spec.rb b/spec/esse/active_record/model/esse_callback_spec.rb new file mode 100644 index 0000000..1233deb --- /dev/null +++ b/spec/esse/active_record/model/esse_callback_spec.rb @@ -0,0 +1,218 @@ +require 'spec_helper' + +class DummyCallbackRepo + def self.add(*vals) + Thread.current[:dummy_callback_repo] ||= Set.new + Thread.current[:dummy_callback_repo].add(*vals) + end + + def self.all + (Thread.current[:dummy_callback_repo] || []).to_a + end + + def self.clear + Thread.current[:dummy_callback_repo] = nil + end +end +class DumpTempCallback < Esse::ActiveRecord::Callback + def call(model) + DummyCallbackRepo.add [model, options, block_result] + end +end +class DumpTempCallbackOnCreate < DumpTempCallback; end +class DumpTempCallbackOnUpdate < DumpTempCallback; end +class DumpTempCallbackOnDestroy < DumpTempCallback; end + +RSpec.describe Esse::ActiveRecord::Model do + let(:model_class) do + Class.new(State) do + include Esse::ActiveRecord::Model + end + end + + before do + DummyCallbackRepo.clear + Thread.current[Esse::ActiveRecord::Hooks::STORE_STATE_KEY] = nil + @__callbacks = Esse::ActiveRecord::Callbacks.instance_variable_get(:@callbacks) + Esse::ActiveRecord::Callbacks.register_callback(:temp, :create, DumpTempCallbackOnCreate) + Esse::ActiveRecord::Callbacks.register_callback(:temp, :update, DumpTempCallbackOnUpdate) + Esse::ActiveRecord::Callbacks.register_callback(:temp, :destroy, DumpTempCallbackOnDestroy) + @__hooks_models = Esse::ActiveRecord::Hooks.models.dup + Esse::ActiveRecord::Hooks.models.clear + stub_cluster_info + + stub_esse_index(:states) do + repository :state, const: true do + document do |state, **| + { + _id: state.id, + name: state.name, + } + end + end + end + end + + after do + Esse::ActiveRecord::Hooks.instance_variable_set(:@models, @__hooks_models) # rubocop:disable RSpec/InstanceVariable + Esse::ActiveRecord::Callbacks.instance_variable_set(:@callbacks, @__callbacks) # rubocop:disable RSpec/InstanceVariable + clean_db + end + + describe '.esse_callbacks' do + it 'returns an empty hash' do + expect(model_class.esse_callbacks).to eq({}) + end + + it 'returns a frozen hash' do + expect(model_class.esse_callbacks).to be_frozen + end + end + + context 'when on :create' do + it 'raises an error when the callback is not registered' do + expect { + model_class.esse_callback 'states:state', :missing_callback, on: %i[create] + }.to raise_error(ArgumentError).with_message(/callback missing_callback for create operation not registered/) + end + + it 'register the model class into Esse::ActiveRecord::Hooks.models' do + model_class.esse_callback 'states:state', :temp, on: %i[create] + expect(Esse::ActiveRecord::Hooks.models).to include(model_class) + end + + it 'register the callback with block definition and custom options' do + model_class.esse_callback('states:state', :temp, on: %i[create], custom: 'value') { :ok } + expect(model_class.esse_callbacks).to a_hash_including( + 'states:state' => a_hash_including( + temp_on_create: match_array([DumpTempCallbackOnCreate, {custom: 'value'}, an_instance_of(Proc)]), + ) + ) + end + + it 'calls the callback with the model instance' do + model_class.esse_callback('states:state', :temp, on: %i[create]) + model = build_record(model_class, name: 'Illinois', id: SecureRandom.uuid) + model.save + expect(DummyCallbackRepo.all).to include([model, {}, nil]) + end + + it 'does not call the callback when the hooks are globally disabled' do + model_class.esse_callback('states:state', :temp, on: %i[create]) + model = build_record(model_class, name: 'Illinois', id: SecureRandom.uuid) + Esse::ActiveRecord::Hooks.without_indexing do + model.save + end + expect(DummyCallbackRepo.all).to be_empty + end + + it 'does not call the callback when the hooks are disabled for the model' do + model_class.esse_callback('states:state', :temp, on: %i[create]) + model = build_record(model_class, name: 'Illinois', id: SecureRandom.uuid) + model_class.without_indexing do + model.save + end + expect(DummyCallbackRepo.all).to be_empty + end + end + + context 'when on :update' do + it 'raises an error when the callback is not registered' do + expect { + model_class.esse_callback 'states:state', :missing_callback, on: %i[update] + }.to raise_error(ArgumentError).with_message(/callback missing_callback for update operation not registered/) + end + + it 'register the model class into Esse::ActiveRecord::Hooks.models' do + model_class.esse_callback 'states:state', :temp, on: %i[update] + expect(Esse::ActiveRecord::Hooks.models).to include(model_class) + end + + it 'register the callback with block definition and custom options' do + model_class.esse_callback('states:state', :temp, on: %i[update], custom: 'value') { :ok } + expect(model_class.esse_callbacks).to a_hash_including( + 'states:state' => a_hash_including( + temp_on_update: match_array([DumpTempCallbackOnUpdate, {custom: 'value'}, an_instance_of(Proc)]), + ) + ) + end + + it 'calls the callback with the model instance' do + model_class.esse_callback('states:state', :temp, on: %i[update]) + model = build_record(model_class, name: 'Illinois', id: SecureRandom.uuid) + model.save + model.update(name: 'New Illinois') + expect(DummyCallbackRepo.all).to include([model, {}, nil]) + end + + it 'does not call the callback when the hooks are globally disabled' do + model_class.esse_callback('states:state', :temp, on: %i[update]) + model = build_record(model_class, name: 'Illinois', id: SecureRandom.uuid) + model.save + Esse::ActiveRecord::Hooks.without_indexing do + model.update(name: 'New Illinois') + end + expect(DummyCallbackRepo.all).to be_empty + end + + it 'does not call the callback when the hooks are disabled for the model' do + model_class.esse_callback('states:state', :temp, on: %i[update]) + model = build_record(model_class, name: 'Illinois', id: SecureRandom.uuid) + model.save + model_class.without_indexing do + model.update(name: 'New Illinois') + end + expect(DummyCallbackRepo.all).to be_empty + end + end + + context 'when on :destroy' do + it 'raises an error when the callback is not registered' do + expect { + model_class.esse_callback 'states:state', :missing_callback, on: %i[destroy] + }.to raise_error(ArgumentError).with_message(/callback missing_callback for destroy operation not registered/) + end + + it 'register the model class into Esse::ActiveRecord::Hooks.models' do + model_class.esse_callback 'states:state', :temp, on: %i[destroy] + expect(Esse::ActiveRecord::Hooks.models).to include(model_class) + end + + it 'register the callback with block definition and custom options' do + model_class.esse_callback('states:state', :temp, on: %i[destroy], custom: 'value') { :ok } + expect(model_class.esse_callbacks).to a_hash_including( + 'states:state' => a_hash_including( + temp_on_destroy: match_array([DumpTempCallbackOnDestroy, {custom: 'value'}, an_instance_of(Proc)]), + ) + ) + end + + it 'calls the callback with the model instance' do + model_class.esse_callback('states:state', :temp, on: %i[destroy]) + model = build_record(model_class, name: 'Illinois', id: SecureRandom.uuid) + model.save + model.destroy + expect(DummyCallbackRepo.all).to include([model, {}, nil]) + end + + it 'does not call the callback when the hooks are globally disabled' do + model_class.esse_callback('states:state', :temp, on: %i[destroy]) + model = build_record(model_class, name: 'Illinois', id: SecureRandom.uuid) + model.save + Esse::ActiveRecord::Hooks.without_indexing do + model.destroy + end + expect(DummyCallbackRepo.all).to be_empty + end + + it 'does not call the callback when the hooks are disabled for the model' do + model_class.esse_callback('states:state', :temp, on: %i[destroy]) + model = build_record(model_class, name: 'Illinois', id: SecureRandom.uuid) + model.save + model_class.without_indexing do + model.destroy + end + expect(DummyCallbackRepo.all).to be_empty + end + end +end diff --git a/spec/esse/active_record/model/index_callback_spec.rb b/spec/esse/active_record/model/index_callback_spec.rb index fd47492..8a58324 100644 --- a/spec/esse/active_record/model/index_callback_spec.rb +++ b/spec/esse/active_record/model/index_callback_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -RSpec.describe Esse::ActiveRecord::Model do +RSpec.describe Esse::ActiveRecord::Model, '.index_callback' do let(:backend_proxy) { double } before do From 4b313c6206bf7e197f7674bd7a99c11e7736db49 Mon Sep 17 00:00:00 2001 From: "Marcos G. Zimmermann" Date: Wed, 10 Jul 2024 16:23:16 -0300 Subject: [PATCH 11/15] chore: add tests to the update_lazy_attribute_callback callback --- .../active_record/model/esse_callback_spec.rb | 2 +- .../update_lazy_attribute_callback_spec.rb | 92 +++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 spec/esse/active_record/model/update_lazy_attribute_callback_spec.rb diff --git a/spec/esse/active_record/model/esse_callback_spec.rb b/spec/esse/active_record/model/esse_callback_spec.rb index 1233deb..28f5c06 100644 --- a/spec/esse/active_record/model/esse_callback_spec.rb +++ b/spec/esse/active_record/model/esse_callback_spec.rb @@ -23,7 +23,7 @@ class DumpTempCallbackOnCreate < DumpTempCallback; end class DumpTempCallbackOnUpdate < DumpTempCallback; end class DumpTempCallbackOnDestroy < DumpTempCallback; end -RSpec.describe Esse::ActiveRecord::Model do +RSpec.describe Esse::ActiveRecord::Model, '.esse_callback' do let(:model_class) do Class.new(State) do include Esse::ActiveRecord::Model diff --git a/spec/esse/active_record/model/update_lazy_attribute_callback_spec.rb b/spec/esse/active_record/model/update_lazy_attribute_callback_spec.rb new file mode 100644 index 0000000..8a401fd --- /dev/null +++ b/spec/esse/active_record/model/update_lazy_attribute_callback_spec.rb @@ -0,0 +1,92 @@ +require 'spec_helper' + +RSpec.describe Esse::ActiveRecord::Model, '.update_lazy_attribute_callback' do + let(:model_class) do + Class.new(State) do + include Esse::ActiveRecord::Model + end + end + + before do + Thread.current[Esse::ActiveRecord::Hooks::STORE_STATE_KEY] = nil + + @__hooks_models = Esse::ActiveRecord::Hooks.models.dup + Esse::ActiveRecord::Hooks.models.clear + stub_cluster_info + + stub_esse_index(:states) do + repository :state, const: true do + document do |state, **| + { + _id: state.id, + name: state.name, + } + end + end + end + end + + after do + Esse::ActiveRecord::Hooks.instance_variable_set(:@models, @__hooks_models) # rubocop:disable RSpec/InstanceVariable + clean_db + end + + it 'register the callback with multiple events' do + model_class.update_lazy_attribute_callback('states:state', :field, custom: 'value') { :ok } + expect(model_class.esse_callbacks).to a_hash_including( + 'states:state' => a_hash_including( + update_lazy_attribute_on_create: match_array([Esse::ActiveRecord::Callbacks::UpdateLazyAttribute, { attribute_name: :field, custom: 'value'}, an_instance_of(Proc)]), + update_lazy_attribute_on_update: match_array([Esse::ActiveRecord::Callbacks::UpdateLazyAttribute, { attribute_name: :field, custom: 'value'}, an_instance_of(Proc)]), + update_lazy_attribute_on_destroy: match_array([Esse::ActiveRecord::Callbacks::UpdateLazyAttribute, { attribute_name: :field, custom: 'value'}, an_instance_of(Proc)]), + ) + ) + end + + context 'when on :create' do + it 'register the model class into Esse::ActiveRecord::Hooks.models' do + model_class.update_lazy_attribute_callback 'states:state', :field, on: %i[create] + expect(Esse::ActiveRecord::Hooks.models).to include(model_class) + end + + it 'register the callback with block definition and custom options' do + model_class.update_lazy_attribute_callback('states:state', :field, on: %i[create], custom: 'value') { :ok } + expect(model_class.esse_callbacks).to a_hash_including( + 'states:state' => a_hash_including( + update_lazy_attribute_on_create: match_array([Esse::ActiveRecord::Callbacks::UpdateLazyAttribute, { attribute_name: :field, custom: 'value'}, an_instance_of(Proc)]), + ) + ) + end + end + + context 'when on :update' do + it 'register the model class into Esse::ActiveRecord::Hooks.models' do + model_class.update_lazy_attribute_callback('states:state', :field, on: %i[update]) + expect(Esse::ActiveRecord::Hooks.models).to include(model_class) + end + + it 'register the callback with block definition and custom options' do + model_class.update_lazy_attribute_callback('states:state', :field, on: %i[update], custom: 'value') { :ok } + expect(model_class.esse_callbacks).to a_hash_including( + 'states:state' => a_hash_including( + update_lazy_attribute_on_update: match_array([Esse::ActiveRecord::Callbacks::UpdateLazyAttribute, { attribute_name: :field, custom: 'value'}, an_instance_of(Proc)]), + ) + ) + end + end + + context 'when on :destroy' do + it 'register the model class into Esse::ActiveRecord::Hooks.models' do + model_class.update_lazy_attribute_callback('states:state', :field, on: %i[destroy]) + expect(Esse::ActiveRecord::Hooks.models).to include(model_class) + end + + it 'register the callback with block definition and custom options' do + model_class.update_lazy_attribute_callback('states:state', :field, on: %i[destroy], custom: 'value') { :ok } + expect(model_class.esse_callbacks).to a_hash_including( + 'states:state' => a_hash_including( + update_lazy_attribute_on_destroy: match_array([Esse::ActiveRecord::Callbacks::UpdateLazyAttribute, { attribute_name: :field, custom: 'value'}, an_instance_of(Proc)]), + ) + ) + end + end +end From 8599982065f45bb93e2db2dda7836c0dd61928cb Mon Sep 17 00:00:00 2001 From: "Marcos G. Zimmermann" Date: Wed, 10 Jul 2024 16:56:08 -0300 Subject: [PATCH 12/15] chore: add tests to update_lazy_attribute_callback callback --- .../callbacks/update_lazy_attribute_spec.rb | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 spec/esse/active_record/callbacks/update_lazy_attribute_spec.rb diff --git a/spec/esse/active_record/callbacks/update_lazy_attribute_spec.rb b/spec/esse/active_record/callbacks/update_lazy_attribute_spec.rb new file mode 100644 index 0000000..8ce30de --- /dev/null +++ b/spec/esse/active_record/callbacks/update_lazy_attribute_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Esse::ActiveRecord::Callbacks::UpdateLazyAttribute do + let(:repo) { double('repo') } + + describe '.initialize' do + it 'sets attribute_name' do + callback = described_class.new(repo: repo, attribute_name: :foo) + expect(callback.attribute_name).to eq(:foo) + end + + it 'sets options' do + callback = described_class.new(repo: repo, attribute_name: :foo, foo: :bar) + expect(callback.options).to eq(foo: :bar) + end + end + + describe '.call' do + let(:ok_response) { { 'result' => 'indexed' } } + let(:county_class) do + Class.new(County) do + include Esse::ActiveRecord::Model + update_lazy_attribute_callback 'states:state', :total_counties do + state_id + end + end + end + + before do + stub_cluster_info + stub_esse_index(:states) do + repository :state, const: true do + document do |state, **| + { + _id: state.id, + name: state.name, + } + end + lazy_document_attribute :total_counties do |docs| + ::County.where(state_id: docs.map(&:id)).group(:state_id).count + end + end + end + end + + after do + clean_db + end + + it 'bulk update the state :total_counties attribute when the county is created' do + state = create_record(State, name: 'Illinois') + county = build_record(county_class, name: 'Cook', state: state) + expect(StatesIndex::State).to receive(:update_documents_attribute).with(:total_counties, state.id, **{}).and_call_original + expect(StatesIndex).to esse_receive_request(:bulk).with( + index: StatesIndex.index_name, + body: [ + { update: { _id: state.id, data: { doc: { total_counties: 1 } } } } + ] + ).and_return(ok_response) + + county.save! + end + end +end From a1681c4cbddb0ffd8302970f22e8007d0e7726ca Mon Sep 17 00:00:00 2001 From: "Marcos G. Zimmermann" Date: Wed, 10 Jul 2024 17:07:32 -0300 Subject: [PATCH 13/15] chore: install esse from rubygems --- Gemfile | 2 +- Gemfile.lock | 18 ++++++------------ ci/Gemfile.rails-5.2.lock | 6 +++--- ci/Gemfile.rails-6.0.lock | 6 +++--- ci/Gemfile.rails-6.1.lock | 6 +++--- ci/Gemfile.rails-7.0.lock | 6 +++--- ci/Gemfile.rails-7.1.lock | 6 +++--- esse-active_record.gemspec | 2 +- lib/esse/active_record/version.rb | 2 +- 9 files changed, 24 insertions(+), 30 deletions(-) diff --git a/Gemfile b/Gemfile index 736b545..3afd56b 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ source 'https://rubygems.org' git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } -gem 'esse', github: 'marcosgz/esse', branch: 'main' +gem 'esse', '~> 0.3.0' gem 'sqlite3', '~> 1.7.3' gem 'activerecord', '~> 5.2' gem 'esse-rspec', '~> 0.0.6' diff --git a/Gemfile.lock b/Gemfile.lock index ff8eec1..70b867d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,18 +1,9 @@ -GIT - remote: https://github.com/marcosgz/esse - revision: 9d9e0fdf92fc72f834b9dd50bf242878f088a23d - branch: main - specs: - esse (0.3.0) - multi_json - thor (>= 0.19) - PATH remote: . specs: - esse-active_record (0.2.1) + esse-active_record (0.3.0) activerecord (>= 4.2, < 8) - esse (>= 0.2.3) + esse (>= 0.3.0) GEM remote: https://rubygems.org/ @@ -48,6 +39,9 @@ GEM elasticsearch-transport (7.17.10) faraday (>= 1, < 3) multi_json + esse (0.3.0) + multi_json + thor (>= 0.19) esse-rspec (0.0.6) esse (>= 0.2.4) rspec (>= 3) @@ -148,7 +142,7 @@ DEPENDENCIES awesome_print dotenv elasticsearch (~> 7.17, >= 7.17.10) - esse! + esse (~> 0.3.0) esse-active_record! esse-rspec (~> 0.0.6) pry diff --git a/ci/Gemfile.rails-5.2.lock b/ci/Gemfile.rails-5.2.lock index 1be0d7d..d407f29 100644 --- a/ci/Gemfile.rails-5.2.lock +++ b/ci/Gemfile.rails-5.2.lock @@ -1,9 +1,9 @@ PATH remote: .. specs: - esse-active_record (0.2.1) + esse-active_record (0.3.0) activerecord (>= 4.2, < 8) - esse (>= 0.2.3) + esse (>= 0.3.0) GEM remote: https://rubygems.org/ @@ -39,7 +39,7 @@ GEM elasticsearch-transport (7.17.10) faraday (>= 1, < 3) multi_json - esse (0.2.6) + esse (0.3.0) multi_json thor (>= 0.19) esse-rspec (0.0.6) diff --git a/ci/Gemfile.rails-6.0.lock b/ci/Gemfile.rails-6.0.lock index c1ddce3..ec3274b 100644 --- a/ci/Gemfile.rails-6.0.lock +++ b/ci/Gemfile.rails-6.0.lock @@ -1,9 +1,9 @@ PATH remote: .. specs: - esse-active_record (0.2.1) + esse-active_record (0.3.0) activerecord (>= 4.2, < 8) - esse (>= 0.2.3) + esse (>= 0.3.0) GEM remote: https://rubygems.org/ @@ -37,7 +37,7 @@ GEM elasticsearch-transport (7.17.10) faraday (>= 1, < 3) multi_json - esse (0.2.6) + esse (0.3.0) multi_json thor (>= 0.19) esse-rspec (0.0.6) diff --git a/ci/Gemfile.rails-6.1.lock b/ci/Gemfile.rails-6.1.lock index abb1757..0eb93bc 100644 --- a/ci/Gemfile.rails-6.1.lock +++ b/ci/Gemfile.rails-6.1.lock @@ -1,9 +1,9 @@ PATH remote: .. specs: - esse-active_record (0.2.1) + esse-active_record (0.3.0) activerecord (>= 4.2, < 8) - esse (>= 0.2.3) + esse (>= 0.3.0) GEM remote: https://rubygems.org/ @@ -37,7 +37,7 @@ GEM elasticsearch-transport (7.17.10) faraday (>= 1, < 3) multi_json - esse (0.2.6) + esse (0.3.0) multi_json thor (>= 0.19) esse-rspec (0.0.6) diff --git a/ci/Gemfile.rails-7.0.lock b/ci/Gemfile.rails-7.0.lock index c1ddce3..ec3274b 100644 --- a/ci/Gemfile.rails-7.0.lock +++ b/ci/Gemfile.rails-7.0.lock @@ -1,9 +1,9 @@ PATH remote: .. specs: - esse-active_record (0.2.1) + esse-active_record (0.3.0) activerecord (>= 4.2, < 8) - esse (>= 0.2.3) + esse (>= 0.3.0) GEM remote: https://rubygems.org/ @@ -37,7 +37,7 @@ GEM elasticsearch-transport (7.17.10) faraday (>= 1, < 3) multi_json - esse (0.2.6) + esse (0.3.0) multi_json thor (>= 0.19) esse-rspec (0.0.6) diff --git a/ci/Gemfile.rails-7.1.lock b/ci/Gemfile.rails-7.1.lock index 90103a0..269a7c4 100644 --- a/ci/Gemfile.rails-7.1.lock +++ b/ci/Gemfile.rails-7.1.lock @@ -1,9 +1,9 @@ PATH remote: .. specs: - esse-active_record (0.2.1) + esse-active_record (0.3.0) activerecord (>= 4.2, < 8) - esse (>= 0.2.3) + esse (>= 0.3.0) GEM remote: https://rubygems.org/ @@ -47,7 +47,7 @@ GEM elasticsearch-transport (7.17.10) faraday (>= 1, < 3) multi_json - esse (0.2.6) + esse (0.3.0) multi_json thor (>= 0.19) esse-rspec (0.0.6) diff --git a/esse-active_record.gemspec b/esse-active_record.gemspec index 2bc5e4d..8eeb045 100644 --- a/esse-active_record.gemspec +++ b/esse-active_record.gemspec @@ -29,7 +29,7 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } spec.require_paths = ['lib'] - spec.add_dependency 'esse', '>= 0.2.3' + spec.add_dependency 'esse', '>= 0.3.0' spec.add_dependency 'activerecord', '>= 4.2', '< 8' spec.add_development_dependency 'awesome_print' spec.add_development_dependency 'dotenv' diff --git a/lib/esse/active_record/version.rb b/lib/esse/active_record/version.rb index e07e50e..d05e0ab 100644 --- a/lib/esse/active_record/version.rb +++ b/lib/esse/active_record/version.rb @@ -2,6 +2,6 @@ module Esse module ActiveRecord - VERSION = '0.2.1' + VERSION = '0.3.0' end end From c9a3b1d6de1dc6a3bf8910aedafa012dc53d5524 Mon Sep 17 00:00:00 2001 From: "Marcos G. Zimmermann" Date: Wed, 10 Jul 2024 17:13:01 -0300 Subject: [PATCH 14/15] chore: fix rubocop offenses --- spec/esse/active_record/callbacks_spec.rb | 4 ++-- spec/esse/active_record/model/esse_callback_spec.rb | 10 +++++++--- .../model/update_lazy_attribute_callback_spec.rb | 12 ++++++------ 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/spec/esse/active_record/callbacks_spec.rb b/spec/esse/active_record/callbacks_spec.rb index 631973e..abab8b7 100644 --- a/spec/esse/active_record/callbacks_spec.rb +++ b/spec/esse/active_record/callbacks_spec.rb @@ -39,12 +39,12 @@ describe '.registered?' do it 'returns false if the callback is not registered' do - expect(described_class.registered?(:external, :create)).to eq(false) + expect(described_class.registered?(:external, :create)).to be(false) end it 'returns true if the callback is registered' do described_class.register_callback(:external, :create, Class.new(Esse::ActiveRecord::Callback)) - expect(described_class.registered?(:external, :create)).to eq(true) + expect(described_class.registered?(:external, :create)).to be(true) end end diff --git a/spec/esse/active_record/model/esse_callback_spec.rb b/spec/esse/active_record/model/esse_callback_spec.rb index 28f5c06..57ed9ac 100644 --- a/spec/esse/active_record/model/esse_callback_spec.rb +++ b/spec/esse/active_record/model/esse_callback_spec.rb @@ -14,13 +14,17 @@ def self.clear Thread.current[:dummy_callback_repo] = nil end end + class DumpTempCallback < Esse::ActiveRecord::Callback def call(model) DummyCallbackRepo.add [model, options, block_result] end end + class DumpTempCallbackOnCreate < DumpTempCallback; end + class DumpTempCallbackOnUpdate < DumpTempCallback; end + class DumpTempCallbackOnDestroy < DumpTempCallback; end RSpec.describe Esse::ActiveRecord::Model, '.esse_callback' do @@ -85,7 +89,7 @@ class DumpTempCallbackOnDestroy < DumpTempCallback; end model_class.esse_callback('states:state', :temp, on: %i[create], custom: 'value') { :ok } expect(model_class.esse_callbacks).to a_hash_including( 'states:state' => a_hash_including( - temp_on_create: match_array([DumpTempCallbackOnCreate, {custom: 'value'}, an_instance_of(Proc)]), + temp_on_create: contain_exactly(DumpTempCallbackOnCreate, {custom: 'value'}, an_instance_of(Proc)), ) ) end @@ -132,7 +136,7 @@ class DumpTempCallbackOnDestroy < DumpTempCallback; end model_class.esse_callback('states:state', :temp, on: %i[update], custom: 'value') { :ok } expect(model_class.esse_callbacks).to a_hash_including( 'states:state' => a_hash_including( - temp_on_update: match_array([DumpTempCallbackOnUpdate, {custom: 'value'}, an_instance_of(Proc)]), + temp_on_update: contain_exactly(DumpTempCallbackOnUpdate, {custom: 'value'}, an_instance_of(Proc)), ) ) end @@ -182,7 +186,7 @@ class DumpTempCallbackOnDestroy < DumpTempCallback; end model_class.esse_callback('states:state', :temp, on: %i[destroy], custom: 'value') { :ok } expect(model_class.esse_callbacks).to a_hash_including( 'states:state' => a_hash_including( - temp_on_destroy: match_array([DumpTempCallbackOnDestroy, {custom: 'value'}, an_instance_of(Proc)]), + temp_on_destroy: contain_exactly(DumpTempCallbackOnDestroy, {custom: 'value'}, an_instance_of(Proc)), ) ) end diff --git a/spec/esse/active_record/model/update_lazy_attribute_callback_spec.rb b/spec/esse/active_record/model/update_lazy_attribute_callback_spec.rb index 8a401fd..8a5dc9d 100644 --- a/spec/esse/active_record/model/update_lazy_attribute_callback_spec.rb +++ b/spec/esse/active_record/model/update_lazy_attribute_callback_spec.rb @@ -35,9 +35,9 @@ model_class.update_lazy_attribute_callback('states:state', :field, custom: 'value') { :ok } expect(model_class.esse_callbacks).to a_hash_including( 'states:state' => a_hash_including( - update_lazy_attribute_on_create: match_array([Esse::ActiveRecord::Callbacks::UpdateLazyAttribute, { attribute_name: :field, custom: 'value'}, an_instance_of(Proc)]), - update_lazy_attribute_on_update: match_array([Esse::ActiveRecord::Callbacks::UpdateLazyAttribute, { attribute_name: :field, custom: 'value'}, an_instance_of(Proc)]), - update_lazy_attribute_on_destroy: match_array([Esse::ActiveRecord::Callbacks::UpdateLazyAttribute, { attribute_name: :field, custom: 'value'}, an_instance_of(Proc)]), + update_lazy_attribute_on_create: contain_exactly(Esse::ActiveRecord::Callbacks::UpdateLazyAttribute, { attribute_name: :field, custom: 'value'}, an_instance_of(Proc)), + update_lazy_attribute_on_update: contain_exactly(Esse::ActiveRecord::Callbacks::UpdateLazyAttribute, { attribute_name: :field, custom: 'value'}, an_instance_of(Proc)), + update_lazy_attribute_on_destroy: contain_exactly(Esse::ActiveRecord::Callbacks::UpdateLazyAttribute, { attribute_name: :field, custom: 'value'}, an_instance_of(Proc)), ) ) end @@ -52,7 +52,7 @@ model_class.update_lazy_attribute_callback('states:state', :field, on: %i[create], custom: 'value') { :ok } expect(model_class.esse_callbacks).to a_hash_including( 'states:state' => a_hash_including( - update_lazy_attribute_on_create: match_array([Esse::ActiveRecord::Callbacks::UpdateLazyAttribute, { attribute_name: :field, custom: 'value'}, an_instance_of(Proc)]), + update_lazy_attribute_on_create: contain_exactly(Esse::ActiveRecord::Callbacks::UpdateLazyAttribute, { attribute_name: :field, custom: 'value'}, an_instance_of(Proc)), ) ) end @@ -68,7 +68,7 @@ model_class.update_lazy_attribute_callback('states:state', :field, on: %i[update], custom: 'value') { :ok } expect(model_class.esse_callbacks).to a_hash_including( 'states:state' => a_hash_including( - update_lazy_attribute_on_update: match_array([Esse::ActiveRecord::Callbacks::UpdateLazyAttribute, { attribute_name: :field, custom: 'value'}, an_instance_of(Proc)]), + update_lazy_attribute_on_update: contain_exactly(Esse::ActiveRecord::Callbacks::UpdateLazyAttribute, { attribute_name: :field, custom: 'value'}, an_instance_of(Proc)), ) ) end @@ -84,7 +84,7 @@ model_class.update_lazy_attribute_callback('states:state', :field, on: %i[destroy], custom: 'value') { :ok } expect(model_class.esse_callbacks).to a_hash_including( 'states:state' => a_hash_including( - update_lazy_attribute_on_destroy: match_array([Esse::ActiveRecord::Callbacks::UpdateLazyAttribute, { attribute_name: :field, custom: 'value'}, an_instance_of(Proc)]), + update_lazy_attribute_on_destroy: contain_exactly(Esse::ActiveRecord::Callbacks::UpdateLazyAttribute, { attribute_name: :field, custom: 'value'}, an_instance_of(Proc)), ) ) end From ba9fab596aa4d98d6faf65999b1abf20cab366a8 Mon Sep 17 00:00:00 2001 From: "Marcos G. Zimmermann" Date: Wed, 10 Jul 2024 17:18:07 -0300 Subject: [PATCH 15/15] chore: fix rubocop offenses --- spec/esse/active_record/callback_spec.rb | 24 +++++++++++++++++++ .../callbacks/update_lazy_attribute_spec.rb | 2 +- spec/esse/active_record/callbacks_spec.rb | 23 ------------------ 3 files changed, 25 insertions(+), 24 deletions(-) create mode 100644 spec/esse/active_record/callback_spec.rb diff --git a/spec/esse/active_record/callback_spec.rb b/spec/esse/active_record/callback_spec.rb new file mode 100644 index 0000000..0b63470 --- /dev/null +++ b/spec/esse/active_record/callback_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +RSpec.describe Esse::ActiveRecord::Callback do + let(:callback_class) do + Class.new(described_class) + end + let(:repo) { double } + + it 'raises an error if #call is not implemented' do + expect { + callback_class.new(repo: repo).call(nil) + }.to raise_error(NotImplementedError, 'You must implement #call method') + end + + it 'has options' do + callback = callback_class.new(repo: repo, foo: 'bar') + expect(callback.options).to eq(foo: 'bar') + end + + it 'has a block result' do + callback = callback_class.new(repo: repo, block_result: 'result') + expect(callback.block_result).to eq('result') + end +end diff --git a/spec/esse/active_record/callbacks/update_lazy_attribute_spec.rb b/spec/esse/active_record/callbacks/update_lazy_attribute_spec.rb index 8ce30de..de7a872 100644 --- a/spec/esse/active_record/callbacks/update_lazy_attribute_spec.rb +++ b/spec/esse/active_record/callbacks/update_lazy_attribute_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Esse::ActiveRecord::Callbacks::UpdateLazyAttribute do - let(:repo) { double('repo') } + let(:repo) { instance_double(Esse::Repository) } describe '.initialize' do it 'sets attribute_name' do diff --git a/spec/esse/active_record/callbacks_spec.rb b/spec/esse/active_record/callbacks_spec.rb index abab8b7..3dd59e7 100644 --- a/spec/esse/active_record/callbacks_spec.rb +++ b/spec/esse/active_record/callbacks_spec.rb @@ -62,26 +62,3 @@ end end end - -RSpec.describe Esse::ActiveRecord::Callback do - let(:callback_class) do - Class.new(described_class) - end - let(:repo) { double } - - it 'raises an error if #call is not implemented' do - expect { - callback_class.new(repo: repo).call(nil) - }.to raise_error(NotImplementedError, 'You must implement #call method') - end - - it 'has options' do - callback = callback_class.new(repo: repo, foo: 'bar') - expect(callback.options).to eq(foo: 'bar') - end - - it 'has a block result' do - callback = callback_class.new(repo: repo, block_result: 'result') - expect(callback.block_result).to eq('result') - end -end