From 9a93eab976217f7bd67a37fa3e31a12800c0e3bc Mon Sep 17 00:00:00 2001 From: Dylan Hoefsloot Date: Fri, 27 Jan 2023 14:49:03 +1300 Subject: [PATCH 01/26] Add transition for initial state This will allow the state machine to track the initial state for the machine, this is important for 3 reasons: - It allows us to keep a record of how long the state machine spent in the initial state - If the initial state configuration changes after the state machine transitions from the initial state we still have an accurate record of what the initial state was - If the initial state configuration changes before the state machine transitions from the initial state we still have an accurate record of what the initial state is --- lib/statesman/machine.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/statesman/machine.rb b/lib/statesman/machine.rb index 153059a6..84771f4b 100644 --- a/lib/statesman/machine.rb +++ b/lib/statesman/machine.rb @@ -233,12 +233,18 @@ def array_to_s_or_nil(input) def initialize(object, options = { transition_class: Statesman::Adapters::MemoryTransition, + initial_transition: false }) @object = object @transition_class = options[:transition_class] @storage_adapter = adapter_class(@transition_class).new( @transition_class, object, self, options ) + + if options[:initial_transition] + @storage_adapter.create(nil, self.class.initial_state, {}) + end + send(:after_initialize) if respond_to? :after_initialize end From a94ed5fda8a476cba8951d6c518ec1ffd14ec139 Mon Sep 17 00:00:00 2001 From: Dylan Hoefsloot Date: Fri, 27 Jan 2023 15:39:25 +1300 Subject: [PATCH 02/26] Fix warning in test --- spec/statesman/adapters/active_record_queries_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/statesman/adapters/active_record_queries_spec.rb b/spec/statesman/adapters/active_record_queries_spec.rb index e3c78c9c..d8fa21d5 100644 --- a/spec/statesman/adapters/active_record_queries_spec.rb +++ b/spec/statesman/adapters/active_record_queries_spec.rb @@ -254,7 +254,7 @@ def self.initial_state; end end it "does not raise an error" do - expect { check_missing_methods! }.to_not raise_exception(NotImplementedError) + expect { check_missing_methods! }.to_not raise_exception end end From b14bda11e5653b9f704b54008a659133c305571b Mon Sep 17 00:00:00 2001 From: Dylan Hoefsloot Date: Fri, 27 Jan 2023 16:10:15 +1300 Subject: [PATCH 03/26] Add tests --- lib/statesman/machine.rb | 4 +-- spec/statesman/machine_spec.rb | 57 ++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/lib/statesman/machine.rb b/lib/statesman/machine.rb index 84771f4b..d7ef09ba 100644 --- a/lib/statesman/machine.rb +++ b/lib/statesman/machine.rb @@ -233,7 +233,7 @@ def array_to_s_or_nil(input) def initialize(object, options = { transition_class: Statesman::Adapters::MemoryTransition, - initial_transition: false + initial_transition: false, }) @object = object @transition_class = options[:transition_class] @@ -242,7 +242,7 @@ def initialize(object, ) if options[:initial_transition] - @storage_adapter.create(nil, self.class.initial_state, {}) + @storage_adapter.create(nil, self.class.initial_state) end send(:after_initialize) if respond_to? :after_initialize diff --git a/spec/statesman/machine_spec.rb b/spec/statesman/machine_spec.rb index c7b8bfb6..211aa21b 100644 --- a/spec/statesman/machine_spec.rb +++ b/spec/statesman/machine_spec.rb @@ -486,6 +486,63 @@ expect(machine_instance.object).to be(my_model) end + context "initial state is configured" do + before { machine.state(:x, initial: true) } + + context "initial_transition is not provided" do + let(:options) { {} } + + it "doesn't call .create on storage adapter" do + expect_any_instance_of(Statesman.storage_adapter).to_not receive(:create) + machine.new(my_model, options) + end + + it "doesn't create a new transition object" do + instance = machine.new(my_model, options) + + expect(instance.history.count).to eq(0) + end + end + + context "initial_transition is provided" do + context "initial_transition is true" do + let(:options) do + { initial_transition: true, + transition_class: Statesman::Adapters::MemoryTransition } + end + + it "calls .create on storage adapter" do + expect_any_instance_of(Statesman.storage_adapter).to receive(:create).with(nil, + "x") + machine.new(my_model, options) + end + + it "creates a new transition object" do + instance = machine.new(my_model, options) + + expect(instance.history.count).to eq(1) + expect(instance.history.first).to be_a(Statesman::Adapters::MemoryTransition) + expect(instance.history.first.to_state).to eq("x") + end + end + + context "initial_transition is false" do + let(:options) { { initial_transition: false } } + + it "doesn't call .create on storage adapter" do + expect_any_instance_of(Statesman.storage_adapter).to_not receive(:create) + machine.new(my_model, options) + end + + it "doesn't create a new transition object" do + instance = machine.new(my_model, options) + + expect(instance.history.count).to eq(0) + end + end + end + end + context "transition class" do it "sets a default" do expect(Statesman.storage_adapter).to receive(:new).once. From c4469a9b7c69e0ad9c02fd5120d4bc1beaab92ff Mon Sep 17 00:00:00 2001 From: Dylan Hoefsloot Date: Fri, 27 Jan 2023 17:40:11 +1300 Subject: [PATCH 04/26] Add additional conditions and tests --- lib/statesman/machine.rb | 4 +- spec/statesman/machine_spec.rb | 90 ++++++++++++++++++++-------------- 2 files changed, 55 insertions(+), 39 deletions(-) diff --git a/lib/statesman/machine.rb b/lib/statesman/machine.rb index d7ef09ba..ab3053eb 100644 --- a/lib/statesman/machine.rb +++ b/lib/statesman/machine.rb @@ -242,7 +242,9 @@ def initialize(object, ) if options[:initial_transition] - @storage_adapter.create(nil, self.class.initial_state) + if history.empty? && self.class.initial_state + @storage_adapter.create(nil, self.class.initial_state) + end end send(:after_initialize) if respond_to? :after_initialize diff --git a/spec/statesman/machine_spec.rb b/spec/statesman/machine_spec.rb index 211aa21b..6b6ef17a 100644 --- a/spec/statesman/machine_spec.rb +++ b/spec/statesman/machine_spec.rb @@ -480,67 +480,81 @@ it_behaves_like "a callback store", :after_guard_failure, :after_guard_failure end + shared_examples "initial transition is not created" do + it "doesn't call .create on storage adapter" do + expect_any_instance_of(Statesman.storage_adapter).to_not receive(:create) + machine.new(my_model, options) + end + end + + shared_examples "initial transition is created" do + it "calls .create on storage adapter" do + expect_any_instance_of(Statesman.storage_adapter).to receive(:create).with(nil, "x") + machine.new(my_model, options) + end + + it "creates a new transition object" do + instance = machine.new(my_model, options) + + expect(instance.history.count).to eq(1) + expect(instance.history.first).to be_a(Statesman::Adapters::MemoryTransition) + expect(instance.history.first.to_state).to eq("x") + end + end + describe "#initialize" do it "accepts an object to manipulate" do machine_instance = machine.new(my_model) expect(machine_instance.object).to be(my_model) end - context "initial state is configured" do - before { machine.state(:x, initial: true) } + context "initial_transition is not provided" do + let(:options) { {} } - context "initial_transition is not provided" do - let(:options) { {} } + it_behaves_like "initial transition is not created" + end - it "doesn't call .create on storage adapter" do - expect_any_instance_of(Statesman.storage_adapter).to_not receive(:create) - machine.new(my_model, options) + context "initial_transition is provided" do + context "initial_transition is true" do + let(:options) do + { initial_transition: true, + transition_class: Statesman::Adapters::MemoryTransition } end - it "doesn't create a new transition object" do - instance = machine.new(my_model, options) + context "history is empty" do + context "initial state is defined" do + before { machine.state(:x, initial: true) } - expect(instance.history.count).to eq(0) - end - end - - context "initial_transition is provided" do - context "initial_transition is true" do - let(:options) do - { initial_transition: true, - transition_class: Statesman::Adapters::MemoryTransition } + it_behaves_like "initial transition is created" end - it "calls .create on storage adapter" do - expect_any_instance_of(Statesman.storage_adapter).to receive(:create).with(nil, - "x") - machine.new(my_model, options) + context "initial state is not defined" do + it_behaves_like "initial transition is not created" end + end - it "creates a new transition object" do - instance = machine.new(my_model, options) - - expect(instance.history.count).to eq(1) - expect(instance.history.first).to be_a(Statesman::Adapters::MemoryTransition) - expect(instance.history.first.to_state).to eq("x") + context "history is not empty" do + before do + allow_any_instance_of(Statesman.storage_adapter).to receive(:history).and_return([{}]) end - end - context "initial_transition is false" do - let(:options) { { initial_transition: false } } + context "initial state is defined" do + before { machine.state(:x, initial: true) } - it "doesn't call .create on storage adapter" do - expect_any_instance_of(Statesman.storage_adapter).to_not receive(:create) - machine.new(my_model, options) + it_behaves_like "initial transition is not created" end - it "doesn't create a new transition object" do - instance = machine.new(my_model, options) - - expect(instance.history.count).to eq(0) + context "initial state is not defined" do + it_behaves_like "initial transition is not created" end end end + + context "initial_transition is false" do + let(:options) { { initial_transition: false } } + + it_behaves_like "initial transition is not created" + end end context "transition class" do From 9c8c35d47ef02d721dd60e4f5c26a4fa9b06c017 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Thu, 16 Mar 2023 12:29:49 +0000 Subject: [PATCH 05/26] Remove unnecessary expectation from spec --- spec/statesman/machine_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/statesman/machine_spec.rb b/spec/statesman/machine_spec.rb index 05375a10..3f966f1a 100644 --- a/spec/statesman/machine_spec.rb +++ b/spec/statesman/machine_spec.rb @@ -497,7 +497,6 @@ instance = machine.new(my_model, options) expect(instance.history.count).to eq(1) - expect(instance.history.first).to be_a(Statesman::Adapters::MemoryTransition) expect(instance.history.first.to_state).to eq("x") end end From a7d06ee1f00e33937e57b922f8e7e5d5b21f4244 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Thu, 16 Mar 2023 12:34:47 +0000 Subject: [PATCH 06/26] Fix rubocop violation --- spec/statesman/machine_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/statesman/machine_spec.rb b/spec/statesman/machine_spec.rb index 3f966f1a..b89d5508 100644 --- a/spec/statesman/machine_spec.rb +++ b/spec/statesman/machine_spec.rb @@ -534,7 +534,8 @@ context "history is not empty" do before do - allow_any_instance_of(Statesman.storage_adapter).to receive(:history).and_return([{}]) + allow_any_instance_of(Statesman.storage_adapter).to receive(:history). + and_return([{}]) end context "initial state is defined" do From db4a118ebd923f761f6b6558a7561814e19189b7 Mon Sep 17 00:00:00 2001 From: Joseph Southan Date: Fri, 3 Nov 2023 14:54:02 +0000 Subject: [PATCH 07/26] Fixup deprecation in Rails 7.2 --- .github/workflows/tests.yml | 2 ++ .ruby-version | 2 +- lib/statesman/adapters/active_record_transition.rb | 6 +++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 56717533..3d24115c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,6 +28,7 @@ jobs: rails-version: - "6.1.5" - "7.0.4" + - "7.1.1" - "main" postgres-version: ["9.6", "11", "14"] exclude: @@ -70,6 +71,7 @@ jobs: rails-version: - "6.1.5" - "7.0.4" + - "7.1.1" - "main" mysql-version: ["5.7", "8.0"] exclude: diff --git a/.ruby-version b/.ruby-version index 944880fa..be94e6f5 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.2.0 +3.2.2 diff --git a/lib/statesman/adapters/active_record_transition.rb b/lib/statesman/adapters/active_record_transition.rb index bdbb6c15..63141001 100644 --- a/lib/statesman/adapters/active_record_transition.rb +++ b/lib/statesman/adapters/active_record_transition.rb @@ -10,7 +10,11 @@ module ActiveRecordTransition extend ActiveSupport::Concern included do - serialize :metadata, JSON + if Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("7.1") + serialize :metadata, coder: JSON + else + serialize :metadata, JSON + end class_attribute :updated_timestamp_column self.updated_timestamp_column = DEFAULT_UPDATED_TIMESTAMP_COLUMN From cfea282535626f467164ddd482c9275e88f2118b Mon Sep 17 00:00:00 2001 From: Joseph Southan Date: Fri, 3 Nov 2023 15:18:50 +0000 Subject: [PATCH 08/26] Fixup more deprecations and remove explict serialises --- Gemfile | 4 +- lib/statesman/adapters/active_record.rb | 9 +---- .../adapters/active_record_transition.rb | 2 +- spec/spec_helper.rb | 4 +- .../adapters/active_record_queries_spec.rb | 2 +- spec/statesman/adapters/active_record_spec.rb | 38 ++++++------------- .../adapters/active_record_transition_spec.rb | 6 ++- .../type_safe_active_record_queries_spec.rb | 2 +- spec/support/active_record.rb | 10 ++--- 9 files changed, 29 insertions(+), 48 deletions(-) diff --git a/Gemfile b/Gemfile index 012d1a45..a258f5dd 100644 --- a/Gemfile +++ b/Gemfile @@ -9,8 +9,8 @@ if ENV['RAILS_VERSION'] == 'main' elsif ENV['RAILS_VERSION'] gem "rails", "~> #{ENV['RAILS_VERSION']}" end + group :development do - # test/unit is no longer bundled with Ruby 2.2, but required by Rails gem "pry" - gem "test-unit", "~> 3.3" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.2.0") + gem "test-unit", "~> 3.3" end diff --git a/lib/statesman/adapters/active_record.rb b/lib/statesman/adapters/active_record.rb index 5176d2af..ff7b3b9f 100644 --- a/lib/statesman/adapters/active_record.rb +++ b/lib/statesman/adapters/active_record.rb @@ -246,13 +246,8 @@ def next_sort_key end def serialized?(transition_class) - if ::ActiveRecord.respond_to?(:gem_version) && - ::ActiveRecord.gem_version >= Gem::Version.new("4.2.0.a") - transition_class.type_for_attribute("metadata"). - is_a?(::ActiveRecord::Type::Serialized) - else - transition_class.serialized_attributes.include?("metadata") - end + transition_class.type_for_attribute("metadata"). + is_a?(::ActiveRecord::Type::Serialized) end def transition_conflict_error?(err) diff --git a/lib/statesman/adapters/active_record_transition.rb b/lib/statesman/adapters/active_record_transition.rb index 63141001..c25eb03d 100644 --- a/lib/statesman/adapters/active_record_transition.rb +++ b/lib/statesman/adapters/active_record_transition.rb @@ -10,7 +10,7 @@ module ActiveRecordTransition extend ActiveSupport::Concern included do - if Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("7.1") + if ::ActiveRecord.gem_version >= Gem::Version.new("7.1") serialize :metadata, coder: JSON else serialize :metadata, JSON diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c269171e..f0775b9e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -40,7 +40,7 @@ def connection_failure ActiveRecord::Migration.verbose = false end - config.before(:each, active_record: true) do + config.before(:each, :active_record) do tables = %w[ my_active_record_models my_active_record_model_transitions @@ -82,7 +82,5 @@ def prepare_sti_transitions_table CreateStiActiveRecordModelTransitionMigration.migrate(:up) StiActiveRecordModelTransition.reset_column_information end - - MyNamespace::MyActiveRecordModelTransition.serialize(:metadata, JSON) end end diff --git a/spec/statesman/adapters/active_record_queries_spec.rb b/spec/statesman/adapters/active_record_queries_spec.rb index e57dc9e6..e3f725d9 100644 --- a/spec/statesman/adapters/active_record_queries_spec.rb +++ b/spec/statesman/adapters/active_record_queries_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -describe Statesman::Adapters::ActiveRecordQueries, active_record: true do +describe Statesman::Adapters::ActiveRecordQueries, :active_record do def configure_old(klass, transition_class) klass.define_singleton_method(:transition_class) { transition_class } klass.define_singleton_method(:initial_state) { :initial } diff --git a/spec/statesman/adapters/active_record_spec.rb b/spec/statesman/adapters/active_record_spec.rb index db55c04b..a0950b4b 100644 --- a/spec/statesman/adapters/active_record_spec.rb +++ b/spec/statesman/adapters/active_record_spec.rb @@ -5,12 +5,12 @@ require "statesman/adapters/shared_examples" require "statesman/exceptions" -describe Statesman::Adapters::ActiveRecord, active_record: true do +describe Statesman::Adapters::ActiveRecord, :active_record do before do prepare_model_table prepare_transitions_table - MyActiveRecordModelTransition.serialize(:metadata, JSON) + # MyActiveRecordModelTransition.serialize(:metadata, JSON) prepare_sti_model_table prepare_sti_transitions_table @@ -38,15 +38,9 @@ allow(metadata_column).to receive_messages(sql_type: "") allow(MyActiveRecordModelTransition).to receive_messages(columns_hash: { "metadata" => metadata_column }) - if ActiveRecord.respond_to?(:gem_version) && - ActiveRecord.gem_version >= Gem::Version.new("4.2.0.a") - expect(MyActiveRecordModelTransition). - to receive(:type_for_attribute).with("metadata"). - and_return(ActiveRecord::Type::Value.new) - else - expect(MyActiveRecordModelTransition). - to receive_messages(serialized_attributes: {}) - end + expect(MyActiveRecordModelTransition). + to receive(:type_for_attribute).with("metadata"). + and_return(ActiveRecord::Type::Value.new) end it "raises an exception" do @@ -91,18 +85,12 @@ allow(metadata_column).to receive_messages(sql_type: "jsonb") allow(MyActiveRecordModelTransition).to receive_messages(columns_hash: { "metadata" => metadata_column }) - if ActiveRecord.respond_to?(:gem_version) && - ActiveRecord.gem_version >= Gem::Version.new("4.2.0.a") - serialized_type = ActiveRecord::Type::Serialized.new( - "", ActiveRecord::Coders::JSON - ) - expect(MyActiveRecordModelTransition). - to receive(:type_for_attribute).with("metadata"). - and_return(serialized_type) - else - expect(MyActiveRecordModelTransition). - to receive_messages(serialized_attributes: { "metadata" => "" }) - end + serialized_type = ActiveRecord::Type::Serialized.new( + "", ActiveRecord::Coders::JSON + ) + expect(MyActiveRecordModelTransition). + to receive(:type_for_attribute).with("metadata"). + and_return(serialized_type) end it "raises an exception" do @@ -467,10 +455,6 @@ CreateNamespacedARModelTransitionMigration.migrate(:up) end - before do - MyNamespace::MyActiveRecordModelTransition.serialize(:metadata, JSON) - end - let(:observer) { double(Statesman::Machine, execute: nil) } let(:model) do MyNamespace::MyActiveRecordModel.create(current_state: :pending) diff --git a/spec/statesman/adapters/active_record_transition_spec.rb b/spec/statesman/adapters/active_record_transition_spec.rb index b321d7eb..d97b8a9b 100644 --- a/spec/statesman/adapters/active_record_transition_spec.rb +++ b/spec/statesman/adapters/active_record_transition_spec.rb @@ -8,7 +8,11 @@ describe "including behaviour" do it "calls Class.serialize" do - expect(transition_class).to receive(:serialize).with(:metadata, JSON).once + if Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("7.1") + expect(transition_class).to receive(:serialize).with(:metadata, coder: JSON).once + else + expect(transition_class).to receive(:serialize).with(:metadata, JSON).once + end transition_class.send(:include, described_class) end end diff --git a/spec/statesman/adapters/type_safe_active_record_queries_spec.rb b/spec/statesman/adapters/type_safe_active_record_queries_spec.rb index c5c68a59..215c7ed6 100644 --- a/spec/statesman/adapters/type_safe_active_record_queries_spec.rb +++ b/spec/statesman/adapters/type_safe_active_record_queries_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -describe Statesman::Adapters::TypeSafeActiveRecordQueries, active_record: true do +describe Statesman::Adapters::TypeSafeActiveRecordQueries, :active_record do def configure(klass, transition_class) klass.send(:extend, described_class) klass.configure_state_machine( diff --git a/spec/support/active_record.rb b/spec/support/active_record.rb index 2b30578c..82d739ed 100644 --- a/spec/support/active_record.rb +++ b/spec/support/active_record.rb @@ -24,7 +24,6 @@ class MyActiveRecordModelTransition < ActiveRecord::Base include Statesman::Adapters::ActiveRecordTransition belongs_to :my_active_record_model - serialize :metadata, JSON end class MyActiveRecordModel < ActiveRecord::Base @@ -51,7 +50,11 @@ class MyActiveRecordModelTransitionWithoutInclude < ActiveRecord::Base self.table_name = "my_active_record_model_transitions" belongs_to :my_active_record_model - serialize :metadata, JSON + if ::ActiveRecord.gem_version >= Gem::Version.new("7.1") + serialize :metadata, coder: JSON + else + serialize :metadata, JSON + end end class CreateMyActiveRecordModelMigration < MIGRATION_CLASS @@ -129,7 +132,6 @@ class OtherActiveRecordModelTransition < ActiveRecord::Base include Statesman::Adapters::ActiveRecordTransition belongs_to :other_active_record_model - serialize :metadata, JSON end class CreateOtherActiveRecordModelMigration < MIGRATION_CLASS @@ -221,7 +223,6 @@ class MyActiveRecordModelTransition < ActiveRecord::Base belongs_to :my_active_record_model, class_name: "MyNamespace::MyActiveRecordModel" - serialize :metadata, JSON def self.table_name_prefix "my_namespace_" @@ -310,7 +311,6 @@ class StiActiveRecordModelTransition < ActiveRecord::Base include Statesman::Adapters::ActiveRecordTransition belongs_to :sti_active_record_model - serialize :metadata, JSON end class StiAActiveRecordModelTransition < StiActiveRecordModelTransition From 3bc3e8c1bb16ea0337743bf127f3b0c6019fe74d Mon Sep 17 00:00:00 2001 From: Joseph Southan Date: Fri, 3 Nov 2023 15:25:53 +0000 Subject: [PATCH 09/26] Update build matrix Deprecate: - Ruby 2.7 - MySql 5.5 - Postgres 9.6..11 --- .github/workflows/tests.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3d24115c..3cc0fd7b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,16 +24,16 @@ jobs: strategy: fail-fast: false matrix: - ruby-version: ["2.7", "3.0", "3.1", "3.2"] + ruby-version: ["3.0", "3.1", "3.2"] rails-version: - - "6.1.5" - - "7.0.4" + - "6.1.7.6" + - "7.0.8" - "7.1.1" - "main" - postgres-version: ["9.6", "11", "14"] + postgres-version: ["12", "13", "14", "15", "16"] exclude: - ruby-version: "3.2" - rails-version: "6.1.5" + rails-version: "6.1.7.6" runs-on: ubuntu-latest services: postgres: @@ -67,16 +67,16 @@ jobs: strategy: fail-fast: false matrix: - ruby-version: ["2.7", "3.0", "3.1", "3.2"] + ruby-version: ["3.0", "3.1", "3.2"] rails-version: - - "6.1.5" - - "7.0.4" + - "6.1.7.6" + - "7.0.8" - "7.1.1" - "main" - mysql-version: ["5.7", "8.0"] + mysql-version: ["8.0", "8.2"] exclude: - ruby-version: 3.2 - rails-version: "6.1.5" + rails-version: "6.1.7.6" runs-on: ubuntu-latest services: mysql: From 517b3521e2cfc35ca67848aec171eebaa2f0732c Mon Sep 17 00:00:00 2001 From: Joseph Southan Date: Fri, 3 Nov 2023 15:32:22 +0000 Subject: [PATCH 10/26] Bump v11.0.0 --- CHANGELOG.md | 9 +++++++++ lib/statesman/version.rb | 2 +- spec/statesman/exceptions_spec.rb | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3eb4a9f7..a94b8ac6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## v11.0.0 3rd November 2023 + +### Changed +- Updated to support ActiveRecord > 7.2 +- Remove support for: + - Ruby; 2.7 + - Postgres; 9.6, 10, 11 + - MySQL; 5.7 + ## v10.2.3 2nd Aug 2023 ### Changed diff --git a/lib/statesman/version.rb b/lib/statesman/version.rb index 62573a4f..bdb77a11 100644 --- a/lib/statesman/version.rb +++ b/lib/statesman/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Statesman - VERSION = "10.2.3" + VERSION = "11.0.0" end diff --git a/spec/statesman/exceptions_spec.rb b/spec/statesman/exceptions_spec.rb index 275a6d60..3f1eefdb 100644 --- a/spec/statesman/exceptions_spec.rb +++ b/spec/statesman/exceptions_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -describe Statesman do +describe "Exceptions" do describe "InvalidStateError" do subject(:error) { Statesman::InvalidStateError.new } From 3c549813dd7df1565e16e401efd2a2121644c428 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Nov 2023 15:56:00 +0000 Subject: [PATCH 11/26] Update rake requirement from ~> 13.0.0 to ~> 13.1.0 Updates the requirements on [rake](https://github.com/ruby/rake) to permit the latest version. - [Release notes](https://github.com/ruby/rake/releases) - [Changelog](https://github.com/ruby/rake/blob/master/History.rdoc) - [Commits](https://github.com/ruby/rake/compare/v13.0.0...v13.1.0) --- updated-dependencies: - dependency-name: rake dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- statesman.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/statesman.gemspec b/statesman.gemspec index a69d1e50..e7255e6d 100644 --- a/statesman.gemspec +++ b/statesman.gemspec @@ -28,7 +28,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency "mysql2", ">= 0.4", "< 0.6" spec.add_development_dependency "pg", ">= 0.18", "<= 1.6" spec.add_development_dependency "rails", ">= 5.2" - spec.add_development_dependency "rake", "~> 13.0.0" + spec.add_development_dependency "rake", "~> 13.1.0" spec.add_development_dependency "rspec", "~> 3.1" spec.add_development_dependency "rspec-github", "~> 2.4.0" spec.add_development_dependency "rspec-its", "~> 1.1" From cd5b2e36d25ed7daa9dad44e1003a9bcb4dd6c0d Mon Sep 17 00:00:00 2001 From: benk-gc Date: Thu, 30 Nov 2023 11:52:26 +0000 Subject: [PATCH 12/26] Add .rspec file. This allows us to remove 'require "spec_helper"' from spec files. --- .gitignore | 3 --- .rspec | 1 + .../statesman/active_record_transition_generator_spec.rb | 1 - spec/generators/statesman/migration_generator_spec.rb | 1 - spec/statesman/adapters/active_record_queries_spec.rb | 2 -- spec/statesman/adapters/active_record_spec.rb | 1 - spec/statesman/adapters/active_record_transition_spec.rb | 1 - spec/statesman/adapters/memory_spec.rb | 1 - spec/statesman/adapters/memory_transition_spec.rb | 1 - spec/statesman/adapters/shared_examples.rb | 2 -- .../statesman/adapters/type_safe_active_record_queries_spec.rb | 2 -- spec/statesman/callback_spec.rb | 2 -- spec/statesman/config_spec.rb | 2 -- spec/statesman/exceptions_spec.rb | 2 -- spec/statesman/guard_spec.rb | 2 -- spec/statesman/machine_spec.rb | 2 -- spec/statesman/utils_spec.rb | 2 -- 17 files changed, 1 insertion(+), 27 deletions(-) create mode 100644 .rspec diff --git a/.gitignore b/.gitignore index b878c414..88a3a80f 100644 --- a/.gitignore +++ b/.gitignore @@ -55,9 +55,6 @@ Gemfile.lock # Used by RuboCop. Remote config files pulled in from inherit_from directive. # .rubocop-https?--* -# Project-specific ignores -.rspec - # VSCode .vscode diff --git a/.rspec b/.rspec new file mode 100644 index 00000000..c99d2e73 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/spec/generators/statesman/active_record_transition_generator_spec.rb b/spec/generators/statesman/active_record_transition_generator_spec.rb index 68431391..73b76703 100644 --- a/spec/generators/statesman/active_record_transition_generator_spec.rb +++ b/spec/generators/statesman/active_record_transition_generator_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require "spec_helper" require "support/generators_shared_examples" require "generators/statesman/active_record_transition_generator" diff --git a/spec/generators/statesman/migration_generator_spec.rb b/spec/generators/statesman/migration_generator_spec.rb index 03966bae..8ff5718b 100644 --- a/spec/generators/statesman/migration_generator_spec.rb +++ b/spec/generators/statesman/migration_generator_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require "spec_helper" require "support/generators_shared_examples" require "generators/statesman/migration_generator" diff --git a/spec/statesman/adapters/active_record_queries_spec.rb b/spec/statesman/adapters/active_record_queries_spec.rb index e3f725d9..9ca8227a 100644 --- a/spec/statesman/adapters/active_record_queries_spec.rb +++ b/spec/statesman/adapters/active_record_queries_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "spec_helper" - describe Statesman::Adapters::ActiveRecordQueries, :active_record do def configure_old(klass, transition_class) klass.define_singleton_method(:transition_class) { transition_class } diff --git a/spec/statesman/adapters/active_record_spec.rb b/spec/statesman/adapters/active_record_spec.rb index a0950b4b..cf2b4d14 100644 --- a/spec/statesman/adapters/active_record_spec.rb +++ b/spec/statesman/adapters/active_record_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require "spec_helper" require "timecop" require "statesman/adapters/shared_examples" require "statesman/exceptions" diff --git a/spec/statesman/adapters/active_record_transition_spec.rb b/spec/statesman/adapters/active_record_transition_spec.rb index d97b8a9b..efbc1b23 100644 --- a/spec/statesman/adapters/active_record_transition_spec.rb +++ b/spec/statesman/adapters/active_record_transition_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require "spec_helper" require "json" describe Statesman::Adapters::ActiveRecordTransition do diff --git a/spec/statesman/adapters/memory_spec.rb b/spec/statesman/adapters/memory_spec.rb index f54df755..e7a1b043 100644 --- a/spec/statesman/adapters/memory_spec.rb +++ b/spec/statesman/adapters/memory_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require "spec_helper" require "statesman/adapters/shared_examples" require "statesman/adapters/memory_transition" diff --git a/spec/statesman/adapters/memory_transition_spec.rb b/spec/statesman/adapters/memory_transition_spec.rb index 467ee68c..731e8694 100644 --- a/spec/statesman/adapters/memory_transition_spec.rb +++ b/spec/statesman/adapters/memory_transition_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require "spec_helper" require "statesman/adapters/memory_transition" describe Statesman::Adapters::MemoryTransition do diff --git a/spec/statesman/adapters/shared_examples.rb b/spec/statesman/adapters/shared_examples.rb index 67a3bcc8..de902bbb 100644 --- a/spec/statesman/adapters/shared_examples.rb +++ b/spec/statesman/adapters/shared_examples.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "spec_helper" - # All adpators must define seven methods: # initialize: Accepts a transition class, parent model and state_attr. # transition_class: Returns the transition class object passed to initialize. diff --git a/spec/statesman/adapters/type_safe_active_record_queries_spec.rb b/spec/statesman/adapters/type_safe_active_record_queries_spec.rb index 215c7ed6..263a425d 100644 --- a/spec/statesman/adapters/type_safe_active_record_queries_spec.rb +++ b/spec/statesman/adapters/type_safe_active_record_queries_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "spec_helper" - describe Statesman::Adapters::TypeSafeActiveRecordQueries, :active_record do def configure(klass, transition_class) klass.send(:extend, described_class) diff --git a/spec/statesman/callback_spec.rb b/spec/statesman/callback_spec.rb index 59f2aaba..723d9be1 100644 --- a/spec/statesman/callback_spec.rb +++ b/spec/statesman/callback_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "spec_helper" - describe Statesman::Callback do let(:cb_lambda) { -> {} } let(:callback) do diff --git a/spec/statesman/config_spec.rb b/spec/statesman/config_spec.rb index f693e4ea..bc11f7b4 100644 --- a/spec/statesman/config_spec.rb +++ b/spec/statesman/config_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "spec_helper" - describe Statesman::Config do let(:instance) { described_class.new } diff --git a/spec/statesman/exceptions_spec.rb b/spec/statesman/exceptions_spec.rb index 3f1eefdb..36e0b7d9 100644 --- a/spec/statesman/exceptions_spec.rb +++ b/spec/statesman/exceptions_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "spec_helper" - describe "Exceptions" do describe "InvalidStateError" do subject(:error) { Statesman::InvalidStateError.new } diff --git a/spec/statesman/guard_spec.rb b/spec/statesman/guard_spec.rb index 2345665e..26ea061f 100644 --- a/spec/statesman/guard_spec.rb +++ b/spec/statesman/guard_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "spec_helper" - describe Statesman::Guard do let(:callback) { -> {} } let(:guard) { described_class.new(from: nil, to: nil, callback: callback) } diff --git a/spec/statesman/machine_spec.rb b/spec/statesman/machine_spec.rb index 6e7cfd3a..b8527ffa 100644 --- a/spec/statesman/machine_spec.rb +++ b/spec/statesman/machine_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "spec_helper" - describe Statesman::Machine do let(:machine) { Class.new { include Statesman::Machine } } let(:my_model) { Class.new { attr_accessor :current_state }.new } diff --git a/spec/statesman/utils_spec.rb b/spec/statesman/utils_spec.rb index b3e76b8b..3bb68b63 100644 --- a/spec/statesman/utils_spec.rb +++ b/spec/statesman/utils_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "spec_helper" - describe Statesman::Utils do describe ".rails_major_version" do subject { described_class.rails_major_version } From 6e499a8fd0275ae39b67a841564ae1dccca072a8 Mon Sep 17 00:00:00 2001 From: Ben Kyriakou <74675306+benk-gc@users.noreply.github.com> Date: Thu, 30 Nov 2023 12:03:48 +0000 Subject: [PATCH 13/26] Add multi-database support to Statesman. (#522) Statesman currently relies heavily on ActiveRecord::Base, either explicitly or implicitly, when querying database connections. This doesn't play well when we have models or transitions which live on a different database, since it forces us to open a query to the primary connection pool. This is a series of changes to allow Statesman to use the context it has available for the model or transition class, and make use of the appropriate connection. Co-authored-by: Amey Kusurkar --- lib/generators/statesman/generator_helpers.rb | 2 +- lib/statesman.rb | 6 +- lib/statesman/adapters/active_record.rb | 50 ++++++++-------- .../adapters/active_record_queries.rb | 2 +- lib/statesman/config.rb | 13 +--- lib/tasks/statesman.rake | 4 +- ...active_record_transition_generator_spec.rb | 7 +++ .../statesman/migration_generator_spec.rb | 5 ++ spec/spec_helper.rb | 38 ++++++++++-- spec/statesman/adapters/active_record_spec.rb | 60 +++++++++++++++---- spec/support/active_record.rb | 58 +++++++++++++++--- spec/support/exactly_query_databases.rb | 35 +++++++++++ 12 files changed, 211 insertions(+), 69 deletions(-) create mode 100644 spec/support/exactly_query_databases.rb diff --git a/lib/generators/statesman/generator_helpers.rb b/lib/generators/statesman/generator_helpers.rb index 147d5d71..b6c1a546 100644 --- a/lib/generators/statesman/generator_helpers.rb +++ b/lib/generators/statesman/generator_helpers.rb @@ -52,7 +52,7 @@ def configuration end def database_supports_partial_indexes? - Statesman::Adapters::ActiveRecord.database_supports_partial_indexes? + Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(klass.constantize) end def metadata_default_value diff --git a/lib/statesman.rb b/lib/statesman.rb index 8890039f..57b7821c 100644 --- a/lib/statesman.rb +++ b/lib/statesman.rb @@ -34,10 +34,8 @@ def self.storage_adapter @storage_adapter || Adapters::Memory end - def self.mysql_gaplock_protection? - return @mysql_gaplock_protection unless @mysql_gaplock_protection.nil? - - @mysql_gaplock_protection = config.mysql_gaplock_protection? + def self.mysql_gaplock_protection?(connection) + config.mysql_gaplock_protection?(connection) end def self.config diff --git a/lib/statesman/adapters/active_record.rb b/lib/statesman/adapters/active_record.rb index ff7b3b9f..1ee46417 100644 --- a/lib/statesman/adapters/active_record.rb +++ b/lib/statesman/adapters/active_record.rb @@ -7,19 +7,15 @@ module Adapters class ActiveRecord JSON_COLUMN_TYPES = %w[json jsonb].freeze - def self.database_supports_partial_indexes? + def self.database_supports_partial_indexes?(model) # Rails 3 doesn't implement `supports_partial_index?` - if ::ActiveRecord::Base.connection.respond_to?(:supports_partial_index?) - ::ActiveRecord::Base.connection.supports_partial_index? + if model.connection.respond_to?(:supports_partial_index?) + model.connection.supports_partial_index? else - ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL" + model.connection.adapter_name.casecmp("postgresql").zero? end end - def self.adapter_name - ::ActiveRecord::Base.connection.adapter_name.downcase - end - def initialize(transition_class, parent_model, observer, options = {}) serialized = serialized?(transition_class) column_type = transition_class.columns_hash["metadata"].sql_type @@ -88,10 +84,10 @@ def create_transition(from, to, metadata) default_transition_attributes(to, metadata), ) - ::ActiveRecord::Base.transaction(requires_new: true) do + transition_class.transaction(requires_new: true) do @observer.execute(:before, from, to, transition) - if mysql_gaplock_protection? + if mysql_gaplock_protection?(transition_class.connection) # We save the transition first with most_recent falsy, then mark most_recent # true after to avoid letting MySQL acquire a next-key lock which can cause # deadlocks. @@ -130,8 +126,8 @@ def default_transition_attributes(to, metadata) end def add_after_commit_callback(from, to, transition) - ::ActiveRecord::Base.connection.add_transaction_record( - ActiveRecordAfterCommitWrap.new do + transition_class.connection.add_transaction_record( + ActiveRecordAfterCommitWrap.new(transition_class.connection) do @observer.execute(:after_commit, from, to, transition) end, ) @@ -144,7 +140,7 @@ def transitions_for_parent # Sets the given transition most_recent = t while unsetting the most_recent of any # previous transitions. def update_most_recents(most_recent_id = nil) - update = build_arel_manager(::Arel::UpdateManager) + update = build_arel_manager(::Arel::UpdateManager, transition_class) update.table(transition_table) update.where(most_recent_transitions(most_recent_id)) update.set(build_most_recents_update_all_values(most_recent_id)) @@ -152,9 +148,11 @@ def update_most_recents(most_recent_id = nil) # MySQL will validate index constraints across the intermediate result of an # update. This means we must order our update to deactivate the previous # most_recent before setting the new row to be true. - update.order(transition_table[:most_recent].desc) if mysql_gaplock_protection? + if mysql_gaplock_protection?(transition_class.connection) + update.order(transition_table[:most_recent].desc) + end - ::ActiveRecord::Base.connection.update(update.to_sql) + transition_class.connection.update(update.to_sql(transition_class)) end def most_recent_transitions(most_recent_id = nil) @@ -223,7 +221,7 @@ def most_recent_value(most_recent_id) if most_recent_id Arel::Nodes::Case.new. when(transition_table[:id].eq(most_recent_id)).then(db_true). - else(not_most_recent_value).to_sql + else(not_most_recent_value).to_sql(transition_class) else Arel::Nodes::SqlLiteral.new(not_most_recent_value) end @@ -233,11 +231,11 @@ def most_recent_value(most_recent_id) # change in Arel as we move into Rails >6.0. # # https://github.com/rails/rails/commit/7508284800f67b4611c767bff9eae7045674b66f - def build_arel_manager(manager) + def build_arel_manager(manager, engine) if manager.instance_method(:initialize).arity.zero? manager.new else - manager.new(::ActiveRecord::Base) + manager.new(engine) end end @@ -258,7 +256,7 @@ def transition_conflict_error?(err) end def unique_indexes - ::ActiveRecord::Base.connection. + transition_class.connection. indexes(transition_class.table_name). select do |index| next unless index.unique @@ -329,16 +327,16 @@ def default_timezone ::ActiveRecord::Base.default_timezone end - def mysql_gaplock_protection? - Statesman.mysql_gaplock_protection? + def mysql_gaplock_protection?(connection) + Statesman.mysql_gaplock_protection?(connection) end def db_true - ::ActiveRecord::Base.connection.quote(type_cast(true)) + transition_class.connection.quote(type_cast(true)) end def db_false - ::ActiveRecord::Base.connection.quote(type_cast(false)) + transition_class.connection.quote(type_cast(false)) end def db_null @@ -348,7 +346,7 @@ def db_null # Type casting against a column is deprecated and will be removed in Rails 6.2. # See https://github.com/rails/arel/commit/6160bfbda1d1781c3b08a33ec4955f170e95be11 def type_cast(value) - ::ActiveRecord::Base.connection.type_cast(value) + transition_class.connection.type_cast(value) end # Check whether the `most_recent` column allows null values. If it doesn't, set old @@ -368,9 +366,9 @@ def not_most_recent_value(db_cast: true) end class ActiveRecordAfterCommitWrap - def initialize(&block) + def initialize(connection, &block) @callback = block - @connection = ::ActiveRecord::Base.connection + @connection = connection end def self.trigger_transactional_callbacks? diff --git a/lib/statesman/adapters/active_record_queries.rb b/lib/statesman/adapters/active_record_queries.rb index d31d0058..35f6a340 100644 --- a/lib/statesman/adapters/active_record_queries.rb +++ b/lib/statesman/adapters/active_record_queries.rb @@ -153,7 +153,7 @@ def most_recent_transition_alias end def db_true - ::ActiveRecord::Base.connection.quote(true) + model.connection.quote(true) end end end diff --git a/lib/statesman/config.rb b/lib/statesman/config.rb index c18bcf1c..e493e10e 100644 --- a/lib/statesman/config.rb +++ b/lib/statesman/config.rb @@ -15,17 +15,10 @@ def storage_adapter(adapter_class) @adapter_class = adapter_class end - def mysql_gaplock_protection? - return @mysql_gaplock_protection unless @mysql_gaplock_protection.nil? - + def mysql_gaplock_protection?(connection) # If our adapter class suggests we're using mysql, enable gaplock protection by # default. - enable_mysql_gaplock_protection if mysql_adapter?(adapter_class) - @mysql_gaplock_protection - end - - def enable_mysql_gaplock_protection - @mysql_gaplock_protection = true + mysql_adapter?(connection) end private @@ -34,7 +27,7 @@ def mysql_adapter?(adapter_class) adapter_name = adapter_name(adapter_class) return false unless adapter_name - adapter_name.start_with?("mysql") + adapter_name.downcase.start_with?("mysql") end def adapter_name(adapter_class) diff --git a/lib/tasks/statesman.rake b/lib/tasks/statesman.rake index 47fa738b..94636869 100644 --- a/lib/tasks/statesman.rake +++ b/lib/tasks/statesman.rake @@ -21,8 +21,8 @@ namespace :statesman do batch_size = 500 parent_class.find_in_batches(batch_size: batch_size) do |models| - ActiveRecord::Base.transaction(requires_new: true) do - if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes? + transition_class.transaction(requires_new: true) do + if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(transition_class) # Set all transitions' most_recent to FALSE transition_class.where(parent_fk => models.map(&:id)). update_all(most_recent: false, updated_at: updated_at) diff --git a/spec/generators/statesman/active_record_transition_generator_spec.rb b/spec/generators/statesman/active_record_transition_generator_spec.rb index 73b76703..860cbb6d 100644 --- a/spec/generators/statesman/active_record_transition_generator_spec.rb +++ b/spec/generators/statesman/active_record_transition_generator_spec.rb @@ -4,6 +4,13 @@ require "generators/statesman/active_record_transition_generator" describe Statesman::ActiveRecordTransitionGenerator, type: :generator do + before do + stub_const("Bacon", Class.new(ActiveRecord::Base)) + stub_const("BaconTransition", Class.new(ActiveRecord::Base)) + stub_const("Yummy::Bacon", Class.new(ActiveRecord::Base)) + stub_const("Yummy::BaconTransition", Class.new(ActiveRecord::Base)) + end + it_behaves_like "a generator" do let(:migration_name) { "db/migrate/create_bacon_transitions.rb" } end diff --git a/spec/generators/statesman/migration_generator_spec.rb b/spec/generators/statesman/migration_generator_spec.rb index 8ff5718b..410e098e 100644 --- a/spec/generators/statesman/migration_generator_spec.rb +++ b/spec/generators/statesman/migration_generator_spec.rb @@ -4,6 +4,11 @@ require "generators/statesman/migration_generator" describe Statesman::MigrationGenerator, type: :generator do + before do + stub_const("Yummy::Bacon", Class.new(ActiveRecord::Base)) + stub_const("Yummy::BaconTransition", Class.new(ActiveRecord::Base)) + end + it_behaves_like "a generator" do let(:migration_name) { "db/migrate/add_statesman_to_bacon_transitions.rb" } end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f0775b9e..7723ecf3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,13 +5,14 @@ require "mysql2" require "pg" require "active_record" +require "active_record/database_configurations" # We have to include all of Rails to make rspec-rails work require "rails" require "action_view" require "action_dispatch" require "action_controller" require "rspec/rails" -require "support/active_record" +require "support/exactly_query_databases" require "rspec/its" require "pry" @@ -28,10 +29,31 @@ def connection_failure if config.exclusion_filter[:active_record] puts "Skipping ActiveRecord tests" else - # Connect to the database for activerecord tests - db_conn_spec = ENV["DATABASE_URL"] - db_conn_spec ||= { adapter: "sqlite3", database: ":memory:" } - ActiveRecord::Base.establish_connection(db_conn_spec) + current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + + # We have to parse this to a hash since ActiveRecord::Base.configurations + # will only consider a single URL config. + url_config = if ENV["DATABASE_URL"] + ActiveRecord::DatabaseConfigurations::ConnectionUrlResolver. + new(ENV["DATABASE_URL"]).to_hash.merge({ sslmode: "disable" }) + end + + db_config = { + current_env => { + primary: url_config || { + adapter: "sqlite3", + database: "/tmp/statesman.db", + }, + secondary: url_config || { + adapter: "sqlite3", + database: "/tmp/statesman.db", + }, + }, + } + + # Connect to the primary database for activerecord tests. + ActiveRecord::Base.configurations = db_config + ActiveRecord::Base.establish_connection(:primary) db_adapter = ActiveRecord::Base.connection.adapter_name puts "Running with database adapter '#{db_adapter}'" @@ -40,6 +62,8 @@ def connection_failure ActiveRecord::Migration.verbose = false end + # Since our primary and secondary connections point to the same database, we don't + # need to worry about applying these actions to both. config.before(:each, :active_record) do tables = %w[ my_active_record_models @@ -53,6 +77,7 @@ def connection_failure ] tables.each do |table_name| sql = "DROP TABLE IF EXISTS #{table_name};" + ActiveRecord::Base.connection.execute(sql) end @@ -84,3 +109,6 @@ def prepare_sti_transitions_table end end end + +# We have to require this after the databases are configured. +require "support/active_record" diff --git a/spec/statesman/adapters/active_record_spec.rb b/spec/statesman/adapters/active_record_spec.rb index cf2b4d14..8beb7cc6 100644 --- a/spec/statesman/adapters/active_record_spec.rb +++ b/spec/statesman/adapters/active_record_spec.rb @@ -9,8 +9,6 @@ prepare_model_table prepare_transitions_table - # MyActiveRecordModelTransition.serialize(:metadata, JSON) - prepare_sti_model_table prepare_sti_transitions_table @@ -25,8 +23,10 @@ after { Statesman.configure { storage_adapter(Statesman::Adapters::Memory) } } + let(:model_class) { MyActiveRecordModel } + let(:transition_class) { MyActiveRecordModelTransition } let(:observer) { double(Statesman::Machine, execute: nil) } - let(:model) { MyActiveRecordModel.create(current_state: :pending) } + let(:model) { model_class.create(current_state: :pending) } it_behaves_like "an adapter", described_class, MyActiveRecordModelTransition @@ -35,8 +35,8 @@ before do metadata_column = double allow(metadata_column).to receive_messages(sql_type: "") - allow(MyActiveRecordModelTransition).to receive_messages(columns_hash: - { "metadata" => metadata_column }) + allow(MyActiveRecordModelTransition). + to receive_messages(columns_hash: { "metadata" => metadata_column }) expect(MyActiveRecordModelTransition). to receive(:type_for_attribute).with("metadata"). and_return(ActiveRecord::Type::Value.new) @@ -104,13 +104,15 @@ describe "#create" do subject(:transition) { create } - let!(:adapter) do - described_class.new(MyActiveRecordModelTransition, model, observer) - end + let!(:adapter) { described_class.new(transition_class, model, observer) } let(:from) { :x } let(:to) { :y } let(:create) { adapter.create(from, to) } + it "only connects to the primary database" do + expect { create }.to exactly_query_databases({ primary: [:writing] }) + end + context "when there is a race" do it "raises a TransitionConflictError" do adapter2 = adapter.dup @@ -118,7 +120,8 @@ adapter.last adapter2.create(:y, :z) expect { adapter.create(:y, :z) }. - to raise_exception(Statesman::TransitionConflictError) + to raise_exception(Statesman::TransitionConflictError). + and exactly_query_databases({ primary: [:writing] }) end it "does not pollute the state when the transition fails" do @@ -342,12 +345,34 @@ end end end + + context "when using the secondary database" do + let(:model_class) { SecondaryActiveRecordModel } + let(:transition_class) { SecondaryActiveRecordModelTransition } + + it "doesn't connect to the primary database" do + expect { create }.to exactly_query_databases({ secondary: [:writing] }) + expect(adapter.last.to_state).to eq("y") + end + + context "when there is a race" do + it "raises a TransitionConflictError and uses the correct database" do + adapter2 = adapter.dup + adapter2.create(:x, :y) + adapter.last + adapter2.create(:y, :z) + + expect { adapter.create(:y, :z) }. + to raise_exception(Statesman::TransitionConflictError). + and exactly_query_databases({ secondary: [:writing] }) + end + end + end end describe "#last" do - let(:adapter) do - described_class.new(MyActiveRecordModelTransition, model, observer) - end + let(:transition_class) { MyActiveRecordModelTransition } + let(:adapter) { described_class.new(transition_class, model, observer) } context "with a previously looked up transition" do before { adapter.create(:x, :y) } @@ -364,8 +389,19 @@ before { adapter.create(:y, :z, []) } it "retrieves the new transition from the database" do + expect { adapter.last.to_state }.to exactly_query_databases({ primary: [:writing] }) expect(adapter.last.to_state).to eq("z") end + + context "when using the secondary database" do + let(:model_class) { SecondaryActiveRecordModel } + let(:transition_class) { SecondaryActiveRecordModelTransition } + + it "retrieves the new transition from the database" do + expect { adapter.last.to_state }.to exactly_query_databases({ secondary: [:writing] }) + expect(adapter.last.to_state).to eq("z") + end + end end context "when a new transition has been created elsewhere" do diff --git a/spec/support/active_record.rb b/spec/support/active_record.rb index 82d739ed..a8c7dbc0 100644 --- a/spec/support/active_record.rb +++ b/spec/support/active_record.rb @@ -81,7 +81,7 @@ def change t.text :metadata, default: "{}" end - if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes? + if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(ActiveRecord::Base) t.boolean :most_recent, default: true, null: false else t.boolean :most_recent, default: true @@ -98,7 +98,7 @@ def change %i[my_active_record_model_id sort_key], unique: true, name: "sort_key_index" - if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes? + if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(ActiveRecord::Base) add_index :my_active_record_model_transitions, %i[my_active_record_model_id most_recent], unique: true, @@ -134,6 +134,48 @@ class OtherActiveRecordModelTransition < ActiveRecord::Base belongs_to :other_active_record_model end +class SecondaryRecord < ActiveRecord::Base + self.abstract_class = true + + connects_to database: { writing: :secondary, reading: :secondary } +end + +class SecondaryActiveRecordModelTransition < SecondaryRecord + self.table_name = "my_active_record_model_transitions" + + include Statesman::Adapters::ActiveRecordTransition + + belongs_to :my_active_record_model, + class_name: "SecondaryActiveRecordModel", + foreign_key: "my_active_record_model_transition_id" +end + +class SecondaryActiveRecordModel < SecondaryRecord + self.table_name = "my_active_record_models" + + has_many :my_active_record_model_transitions, + class_name: "SecondaryActiveRecordModelTransition", + foreign_key: "my_active_record_model_id", + autosave: false + + alias_method :transitions, :my_active_record_model_transitions + + include Statesman::Adapters::ActiveRecordQueries[ + transition_class: SecondaryActiveRecordModelTransition, + initial_state: :initial + ] + + def state_machine + @state_machine ||= MyStateMachine.new( + self, transition_class: SecondaryActiveRecordModelTransition + ) + end + + def metadata + super || {} + end +end + class CreateOtherActiveRecordModelMigration < MIGRATION_CLASS def change create_table :other_active_record_models do |t| @@ -158,7 +200,7 @@ def change t.text :metadata, default: "{}" end - if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes? + if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(ActiveRecord::Base) t.boolean :most_recent, default: true, null: false else t.boolean :most_recent, default: true @@ -171,7 +213,7 @@ def change %i[other_active_record_model_id sort_key], unique: true, name: "other_sort_key_index" - if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes? + if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(ActiveRecord::Base) add_index :other_active_record_model_transitions, %i[other_active_record_model_id most_recent], unique: true, @@ -253,7 +295,7 @@ def change t.text :metadata, default: "{}" end - if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes? + if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(ActiveRecord::Base) t.boolean :most_recent, default: true, null: false else t.boolean :most_recent, default: true @@ -265,7 +307,7 @@ def change add_index :my_namespace_my_active_record_model_transitions, :sort_key, unique: true, name: "my_namespaced_key" - if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes? + if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(ActiveRecord::Base) add_index :my_namespace_my_active_record_model_transitions, %i[my_active_record_model_id most_recent], unique: true, @@ -342,7 +384,7 @@ def change t.text :metadata, default: "{}" end - if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes? + if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(ActiveRecord::Base) t.boolean :most_recent, default: true, null: false else t.boolean :most_recent, default: true @@ -355,7 +397,7 @@ def change %i[type sti_active_record_model_id sort_key], unique: true, name: "sti_sort_key_index" - if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes? + if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(ActiveRecord::Base) add_index :sti_active_record_model_transitions, %i[type sti_active_record_model_id most_recent], unique: true, diff --git a/spec/support/exactly_query_databases.rb b/spec/support/exactly_query_databases.rb new file mode 100644 index 00000000..209b6231 --- /dev/null +++ b/spec/support/exactly_query_databases.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# `expected_dbs` should be a Hash of the form: +# { +# primary: [:writing, :reading], +# replica: [:reading], +# } +RSpec::Matchers.define :exactly_query_databases do |expected_dbs| + match do |block| + @expected_dbs = expected_dbs.transform_values(&:to_set).with_indifferent_access + @actual_dbs = Hash.new { |h, k| h[k] = Set.new }.with_indifferent_access + + ActiveSupport::Notifications. + subscribe("sql.active_record") do |_name, _start, _finish, _id, payload| + pool = payload.fetch(:connection).pool + + next if pool.is_a?(ActiveRecord::ConnectionAdapters::NullPool) + + name = pool.db_config.name + role = pool.role + + @actual_dbs[name] << role + end + + block.call + + @actual_dbs == @expected_dbs + end + + failure_message do |_block| + "expected to query exactly #{@expected_dbs}, but queried #{@actual_dbs}" + end + + supports_block_expectations +end From bb28d456adffc0c4b3c7aae17ee4e4b0d038db71 Mon Sep 17 00:00:00 2001 From: Ben Kyriakou <74675306+benk-gc@users.noreply.github.com> Date: Thu, 30 Nov 2023 14:39:20 +0000 Subject: [PATCH 14/26] Bump version to 12.0.0. (#525) --- CHANGELOG.md | 6 ++++++ lib/statesman/version.rb | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a94b8ac6..a6b2c6e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## v12.0.0 30th November 2023 + +### Changed +- Added multi-database support [#522](https://github.com/gocardless/statesman/pull/522) + - This now uses the correct ActiveRecord connection for the model or transition in a multi-database environment + ## v11.0.0 3rd November 2023 ### Changed diff --git a/lib/statesman/version.rb b/lib/statesman/version.rb index bdb77a11..45813dcd 100644 --- a/lib/statesman/version.rb +++ b/lib/statesman/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Statesman - VERSION = "11.0.0" + VERSION = "12.0.0" end From 9c7df62c3f4cff321a7b393b541b2272f04764aa Mon Sep 17 00:00:00 2001 From: Jurre Stender Date: Thu, 21 Dec 2023 09:33:06 +0100 Subject: [PATCH 15/26] Enable gaplock protection when using trilogy mysql adapter In https://github.com/gocardless/statesman/pull/522 the ability to enable gaplock protection manually was removed, likely assuming there was no need for this since it's enabled by default when using an adapter that is named `mysql*`, however we use https://github.com/trilogy-libraries/trilogy which has been gaining some traction in the community and this change now leaves us without a way to enable the functionality. I think it makes most sense to enable the functionality by default when using this new adapter as well, and keep config requirements minimal, so I added a little check for that adapter name as well. --- lib/statesman/config.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/statesman/config.rb b/lib/statesman/config.rb index e493e10e..beaea8cb 100644 --- a/lib/statesman/config.rb +++ b/lib/statesman/config.rb @@ -27,7 +27,7 @@ def mysql_adapter?(adapter_class) adapter_name = adapter_name(adapter_class) return false unless adapter_name - adapter_name.downcase.start_with?("mysql") + adapter_name.downcase.start_with?("mysql", "trilogy") end def adapter_name(adapter_class) From d52af9a4778a80a947908045a412d99f0bf86c33 Mon Sep 17 00:00:00 2001 From: Hartley McGuire Date: Thu, 4 Jan 2024 15:45:55 -0500 Subject: [PATCH 16/26] Fix autoloading the VERSION constant While waiting for Statesman to support gap lock protection for Trilogy by default, I wanted to add a conditional monkeypatch to my Rails app based on the version of Statesman loaded. However, trying to reference `Statesman::VERSION` leads to an error: ``` $ irb irb(main):001> require "statesman" => true irb(main):002> Statesman::VERSION (irb):2:in `
': uninitialized constant Statesman::VERSION (NameError) Did you mean? Statesman::Version from /home/hartley/.cache/asdf/installs/ruby/3.2.2/lib/ruby/gems/3.2.0/gems/irb-1.11.0/exe/irb:9:in `' from /home/hartley/.cache/asdf/installs/ruby/3.2.2/bin/irb:25:in `load' from /home/hartley/.cache/asdf/installs/ruby/3.2.2/bin/irb:25:in `
' ``` This commit fixes the issue by changing the autoload for the `version.rb` file to point to `VERSION` instead of `Version` (which does not exist). --- lib/statesman.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/statesman.rb b/lib/statesman.rb index 57b7821c..cf9fe346 100644 --- a/lib/statesman.rb +++ b/lib/statesman.rb @@ -6,7 +6,7 @@ module Statesman autoload :Callback, "statesman/callback" autoload :Guard, "statesman/guard" autoload :Utils, "statesman/utils" - autoload :Version, "statesman/version" + autoload :VERSION, "statesman/version" module Adapters autoload :Memory, "statesman/adapters/memory" autoload :ActiveRecord, "statesman/adapters/active_record" From 9d9e74e295b7678ef00bfcb1091f00a8f79b6b12 Mon Sep 17 00:00:00 2001 From: Stephen Binns Date: Fri, 5 Jan 2024 10:03:13 +0000 Subject: [PATCH 17/26] Fix rubocop violations --- lib/statesman/callback.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/statesman/callback.rb b/lib/statesman/callback.rb index 9c0623ea..5d9f76c1 100644 --- a/lib/statesman/callback.rb +++ b/lib/statesman/callback.rb @@ -40,11 +40,11 @@ def matches_all_transitions end def matches_from_state(from, to) - (from == self.from && (to.nil? || self.to.empty?)) + from == self.from && (to.nil? || self.to.empty?) end def matches_to_state(from, to) - ((from.nil? || self.from.nil?) && self.to.include?(to)) + (from.nil? || self.from.nil?) && self.to.include?(to) end def matches_both_states(from, to) From b7664a3dd339acba8cf2e98447fa821d665f2561 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Jan 2024 10:33:14 +0000 Subject: [PATCH 18/26] Update sqlite3 requirement from ~> 1.6.1 to ~> 1.7.0 (#529) Updates the requirements on [sqlite3](https://github.com/sparklemotion/sqlite3-ruby) to permit the latest version. - [Release notes](https://github.com/sparklemotion/sqlite3-ruby/releases) - [Changelog](https://github.com/sparklemotion/sqlite3-ruby/blob/master/CHANGELOG.md) - [Commits](https://github.com/sparklemotion/sqlite3-ruby/compare/v1.6.1...v1.7.0) --- updated-dependencies: - dependency-name: sqlite3 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- statesman.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/statesman.gemspec b/statesman.gemspec index a69d1e50..22f6d9b1 100644 --- a/statesman.gemspec +++ b/statesman.gemspec @@ -33,7 +33,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency "rspec-github", "~> 2.4.0" spec.add_development_dependency "rspec-its", "~> 1.1" spec.add_development_dependency "rspec-rails", "~> 6.0" - spec.add_development_dependency "sqlite3", "~> 1.6.1" + spec.add_development_dependency "sqlite3", "~> 1.7.0" spec.add_development_dependency "timecop", "~> 0.9.1" spec.metadata = { From 9e5ed2274d20a670f74de0302c1af0bbdc17cf6e Mon Sep 17 00:00:00 2001 From: Dave Gudge Date: Thu, 6 Aug 2020 14:32:39 +0100 Subject: [PATCH 19/26] fix: Ensuring inheritance The call to `ensure_inheritance` can cause a stack overflow when used in combination with the `enumerize` gem and a standard inherited model (not STI - no type column defined on the table). https://github.com/gocardless/statesman/pull/373#discussion_r465877454 There's no need to continue with the `ensure_inheritance` when using a standard inherited model; the statesman methods are already available on the subclass. --- lib/statesman/adapters/active_record_queries.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/statesman/adapters/active_record_queries.rb b/lib/statesman/adapters/active_record_queries.rb index 35f6a340..99d5258e 100644 --- a/lib/statesman/adapters/active_record_queries.rb +++ b/lib/statesman/adapters/active_record_queries.rb @@ -39,7 +39,7 @@ def initialize(**args) end def included(base) - ensure_inheritance(base) + ensure_inheritance(base) unless base.subclasses.none? query_builder = QueryBuilder.new(base, **@args) From 10844907f2279bc814d7187b486707814dece506 Mon Sep 17 00:00:00 2001 From: Dave Gudge Date: Fri, 5 Jan 2024 11:08:26 +0000 Subject: [PATCH 20/26] `ensure_inheritance` condition `subclasses` was added as part of Ruby 3.1 https://ruby-doc.org/core-3.1.0/Class.html#method-i-subclasses Add a `respond_to?` to `base` to ensure that Ruby 3.0 will continue to work as expected. https://github.com/gocardless/statesman/pull/511#discussion_r1442670189 --- lib/statesman/adapters/active_record_queries.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/statesman/adapters/active_record_queries.rb b/lib/statesman/adapters/active_record_queries.rb index 99d5258e..6f486457 100644 --- a/lib/statesman/adapters/active_record_queries.rb +++ b/lib/statesman/adapters/active_record_queries.rb @@ -39,7 +39,7 @@ def initialize(**args) end def included(base) - ensure_inheritance(base) unless base.subclasses.none? + ensure_inheritance(base) if base.respond_to?(:subclasses) && base.subclasses.any? query_builder = QueryBuilder.new(base, **@args) From 2204be2ec6a39e374503ee50f70bb363b1e51fa1 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 5 Jan 2024 11:17:30 +0000 Subject: [PATCH 21/26] Tidy up documentation * Fix all markdownlint warnings where possible and disable them where not * Update URLs for dependabot compatibility score badge in README.md * Ensure CHANGELOG.md uses consistent format --- CHANGELOG.md | 200 +++++++++++++++++++++++++++++++++++++----------- CONTRIBUTING.md | 27 +++---- README.md | 182 ++++++++++++++++++++++++++++--------------- 3 files changed, 290 insertions(+), 119 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6b2c6e1..fa2bb82c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,20 @@ +# Changelog + + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + ## v12.0.0 30th November 2023 -### Changed +### Added + - Added multi-database support [#522](https://github.com/gocardless/statesman/pull/522) - This now uses the correct ActiveRecord connection for the model or transition in a multi-database environment ## v11.0.0 3rd November 2023 ### Changed + - Updated to support ActiveRecord > 7.2 - Remove support for: - Ruby; 2.7 @@ -15,44 +23,55 @@ ## v10.2.3 2nd Aug 2023 -### Changed +### Fixed + - Fixed calls to reloading internal cache is the state_machine was made private / protected ## v10.2.2 21st April 2023 ### Changed + - Calling `active_record.reload` resets the adapater's internal cache ## v10.2.1 3rd April 2023 -### Changed +### Fixed + - Fixed an edge case where `adapter.reset` were failing if the cache is empty ## v10.2.0 3rd April 2023 -### Changed +### Fixed + - Fixed caching of `last_transition` [#505](https://github.com/gocardless/statesman/pull/505) ## v10.1.0 10th March 2023 -### CHanged +### Changed + - Add the source location of the guard callback to `Statesman::GuardFailedError` ## v10.0.0 17th May 2022 -### Changed +### Added + - Added support for Ruby 3.1 [#462](https://github.com/gocardless/statesman/pull/462) -- Removed support for Ruby 2.5 and 2.6 [#462](https://github.com/gocardless/statesman/pull/462) - Added `remove_state` and `remove_transitions` methods to `Statesman::Machine` [#464](https://github.com/gocardless/statesman/pull/464) +### Changed + +- Removed support for Ruby 2.5 and 2.6 [#462](https://github.com/gocardless/statesman/pull/462) + ## v9.0.1 4th February 2021 ### Changed + - Deprecate `ActiveRecord::Base.default_timezone` in favour of `ActiveRecord.default_timezone` [#446](https://github.com/gocardless/statesman/pull/446) ## v9.0.0 9th August 2021 ### Added + - Added Ruby 3.0 support ### Breaking changes @@ -62,19 +81,20 @@ ## v8.0.3 8th June 2021 ### Added + - Implement `Machine#last_transition_to`, to find the last transition to a given state [#438](https://github.com/gocardless/statesman/pull/438) ## v8.0.2 30th March 2021 -### Changed +### Fixed - Fixed a bug where the `history` of a model was left in an incorrect state after a transition conflict [#433](https://github.com/gocardless/statesman/pull/433) ## v8.0.1 20th January 2021 -### Changed +### Fixed - Fixed `no implicit conversion of nil into String` error when quoting null values [#427](https://github.com/gocardless/statesman/pull/427) @@ -118,43 +138,57 @@ ## v7.1.0, 10th Feb 2020 +### Fixed + - Fix `to_s` on `TransitionFailedError` & `GuardFailedError`. `.message` and `.to_s` diverged when `from` and `to` accessors where added in v4.1.3 ## v7.0.1, 8th Jan 2020 +### Fixed + - Fix deprecation warning with Ruby 2.7 [#386](https://github.com/gocardless/statesman/pull/386) ## v7.0.0, 8th Jan 2020 -**Breaking changes** +### Breaking changes - Drop official support for Rails 4.2, 5.0 and 5.1, following our [compatibility policy](https://github.com/gocardless/statesman/blob/master/docs/COMPATIBILITY.md). ## v6.0.0, 20th December 2019 -**Breaking changes** +### Breaking changes - Drop official support for Ruby 2.2 and 2.3 following our [compatibility policy](https://github.com/gocardless/statesman/blob/master/docs/COMPATIBILITY.md). ## v5.2.0, 17th December 2019 +### Changed + - Issue `most_recent_transition_join` query as a single-line string [#381](https://github.com/gocardless/statesman/pull/381) ## v5.1.0, 22th November 2019 +### Fixed + - Correct `Statesman::Adapters::ActiveRecordQueries` error text [@Bramjetten](https://github.com/gocardless/statesman/pull/376) - Removes duplicate `map` call [Isaac Seymour](https://github.com/gocardless/statesman/pull/362) + +### Changed + - Update changelog with instructions of how to use `ActiveRecordQueries` added in v5.0.0 - Pass exception into `after_transition_failure` and `after_guard_failure` callbacks [@credric-cordenier](https://github.com/gocardless/statesman/pull/378) ## v5.0.0, 11th November 2019 +### Added + - Adds new syntax and restrictions to ActiveRecordQueries [PR#358](https://github.com/gocardless/statesman/pull/358). With the introduction of this, defining `self.transition_class` or `self.initial_state` is deprecated and will be removed in the next major release. Change + ```ruby include Statesman::Adapters::ActiveRecordQueries def self.initial_state @@ -164,7 +198,9 @@ MyTransition end ``` + to + ```ruby include Statesman::Adapters::ActiveRecordQueries[ initial_state: :initial, @@ -174,34 +210,51 @@ ## v4.1.4, 11th November 2019 +### Changed + - Reverts the breaking changes from [PR#358](https://github.com/gocardless/statesman/pull/358) & `v4.1.3` that where included in the last minor release. If you have changed your code to work with these changes `v5.0.0` will be a copy of `v4.1.3` with a bugfix applied. ## v4.1.3, 6th November 2019 +### Added + - Add accessible from / to state attributes on the `TransitionFailedError` to avoid parsing strings [@ahjmorton](https://github.com/gocardless/statesman/pull/367) - Add `after_transition_failure` mechanism [@credric-cordenier](https://github.com/gocardless/statesman/pull/366) ## v4.1.2, 17th August 2019 +### Added + - Add support for Rails 6 [@greysteil](https://github.com/gocardless/statesman/pull/360) ## v4.1.1, 6th July 2019 +### Fixed + - Fix statesman index detection for indexes that start t-z [@hmarr](https://github.com/gocardless/statesman/pull/354) - Correct access of metadata via `state_machine` [@glenpike](https://github.com/gocardless/statesman/pull/349) ## v4.1.0, 10 April 2019 -- Add better support for mysql (and others) in `transition_conflict_error?` [@greysteil](https://github.com/greysteil) (https://github.com/gocardless/statesman/pull/342) +### Changed + +- Add better support for mysql (and others) in `transition_conflict_error?` [@greysteil](https://github.com/greysteil) () ## v4.0.0, 22 February 2019 -- Forces Statesman to use a new transactions with `requires_new: true` (https://github.com/gocardless/statesman/pull/249) +### Fixed + - Fixes an issue with `after_commit` transition blocks that where being executed even if the transaction rolled back. ([patch](https://github.com/gocardless/statesman/pull/338) by [@matid](https://github.com/matid)) +### Changed + +- Forces Statesman to use a new transactions with `requires_new: true` () + ## v3.5.0, 2 November 2018 +### Changed + - Expose `most_recent_transition_join` - ActiveRecords `or` requires that both sides of the query match up. Exposing this methods makes things easier if one side of the `or` uses `in_state` or `not_in_state`. (patch by [@adambutler](https://github.com/adambutler)) @@ -209,37 +262,50 @@ ## v3.4.1, 14 February 2018 ❤️ +### Added + - Support ActiveRecord transition classes which don't include `Statesman::Adapters::ActiveRecordTransition`, and thus don't have a `.updated_timestamp_column` method (see #310 for further details) (patch by [@timrogers](https://github.com/timrogers)) ## v3.4.0, 12 February 2018 +### Changed + - When unsetting the `most_recent` flag during a transition, don't assume that transitions have an `updated_at` attribute, but rather allow the "updated timestamp column" to be re-configured or disabled entirely (patch by [@timrogers](https://github.com/timrogers)) ## v3.3.0, 5 January 2018 +### Changed + - Touch `updated_at` on transitions when unsetting `most_recent` flag (patch by [@NGMarmaduke](https://github.com/NGMarmaduke)) - Fix `force_reload` for ActiveRecord models with loaded transitions (patch by [@jacobpgn](https://github.com/)) ## v3.2.0, 27 November 2017 +### Added + - Allow specifying metadata with `Machine#allowed_transitions` (patch by [@vvondra](https://github.com/vvondra)) ## v3.1.0, 1 September 2017 +### Added + - Add support for Rails 5.0.x and 5.1.x (patch by [@kenchan0130](https://github.com/kenchan0130) and [@timrogers](https://github.com/timrogers)) + +### Changed + - Run tests in CircleCI instead of TravisCI (patch by [@timrogers](https://github.com/timrogers)) - Update Rubocop and fix offences (patch by [@timrogers](https://github.com/timrogers)) ## v3.0.0, 3 July 2017 -*Breaking changes* +### Breaking changes - Drop support for Rails < 4.2 - Drop support for Ruby < 2.2 For details on our compatibility policy, see `docs/COMPATIBILITY.md`. -*Changes* +### Changed - Better handling of custom transition association names (patch by [@greysteil](https://github.com/greysteil)) - Add foreign keys to transition table generator (patch by [@greysteil](https://github.com/greysteil)) @@ -247,6 +313,8 @@ For details on our compatibility policy, see `docs/COMPATIBILITY.md`. ## v2.0.1, 29 March 2016 +### Added + - Add support for Rails 5 (excluding Mongoid adapter) ## v2.0.0, 5 January 2016 @@ -255,7 +323,7 @@ For details on our compatibility policy, see `docs/COMPATIBILITY.md`. ## v2.0.0.rc1, 23 December 2015 -*Breaking changes* +### Breaking changes - Unset most_recent after before transitions - TL;DR: set `autosave: false` on the `has_many` association between your parent and transition model and this change will almost certainly not affect your integration @@ -273,7 +341,7 @@ For details on our compatibility policy, see `docs/COMPATIBILITY.md`. - To keep Statesman lightweight we've moved event functionality into the `statesman-events` gem - If you are using events, add `statesman-events` to your gemfile and include `Statesman::Events` in your state machines -*Changes* +### Changed - Add after_destroy hook to ActiveRecord transition model templates - Add `in_state?` instance method to `Statesman::Machine` @@ -281,56 +349,73 @@ For details on our compatibility policy, see `docs/COMPATIBILITY.md`. ## v1.3.1, 2 July 2015 +### Changed + - Fix `in_state` queries with a custom `transition_name` (patch by [0tsuki](https://github.com/0tsuki)) - Fix `backfill_most_recent` rake task for databases that support partial indexes (patch by [greysteil](https://github.com/greysteil)) ## v1.3.0, 20 June 2015 +### Changed + - Rename `last_transition` alias in `ActiveRecordQueries` to `most_recent_#{model_name}`, to allow merging of two such queries (patch by [@isaacseymour](https://github.com/isaacseymour)) ## v1.2.5, 17 June 2015 +### Changed + - Make `backfill_most_recent` rake task db-agnostic (patch by [@timothyp](https://github.com/timothyp)) ## v1.2.4, 16 June 2015 +### Changed + - Clarify error messages when misusing `Statesman::Adapters::ActiveRecordTransition` (patch by [@isaacseymour](https://github.com/isaacseymour)) ## v1.2.3, 14 April 2015 +### Changed + - Fix use of most_recent column in MySQL (partial indexes aren't supported) (patch by [@greysteil](https://github.com/greysteil)) ## v1.2.2, 24 March 2015 +### Added + - Add support for namespaced transition models (patch by [@DanielWright](https://github.com/DanielWright)) ## v1.2.1, 24 March 2015 +### Added + - Add support for Postgres 9.4's `jsonb` column type (patch by [@isaacseymour](https://github.com/isaacseymour)) ## v1.2.0, 18 March 2015 -*Changes* +### Added - Add a `most_recent` column to transition tables to greatly speed up queries (ActiveRecord adapter only). - All queries are backwards-compatible, so everything still works without the new column. - The upgrade path is: - Generate and run a migration for adding the column, by running `rails generate statesman:add_most_recent `. - - Backfill the `most_recent` column on old records by running `rake statesman:backfill_most_recent[ParentModel] `. + - Backfill the `most_recent` column on old records by running `rake statesman:backfill_most_recent[ParentModel]`. - Add constraints and indexes to the transition table that make use of the new field, by running `rails g statesman:add_constraints_to_most_recent `. - The upgrade path has been designed to be zero-downtime, even on large tables. As a result, please note that queries will only use the `most_recent` field after the constraints have been added. -- `ActiveRecordQueries.{not_,}in_state` now accepts an array of states. +### Changed + +- `ActiveRecordQueries.{not_,}in_state` now accepts an array of states. ## v1.1.0, 9 December 2014 -*Fixes* + +### Fixed - Support for Rails 4.2.0.rc2: - Remove use of serialized_attributes when using 4.2+. (patch by [@greysteil](https://github.com/greysteil)) - Use reflect_on_association rather than directly using the reflections hash. (patch by [@timrogers](https://github.com/timrogers)) - Fix `ActiveRecordQueries.in_state` when `Model.initial_state` is defined as a symbol. (patch by [@isaacseymour](https://github.com/isaacseymour)) -*Changes* +### Changed - Transition metadata now defaults to `{}` rather than `nil`. (patch by [@greysteil](https://github.com/greysteil)) @@ -339,99 +424,126 @@ For details on our compatibility policy, see `docs/COMPATIBILITY.md`. No changes from v1.0.0.beta2 ## v1.0.0.beta2, 10 October 2014 -*Breaking changes* + +### Breaking changes - Rename `ActiveRecordModel` to `ActiveRecordQueries`, to reflect the fact that it mixes in some helpful scopes, but is not required. ## v1.0.0.beta1, 9 October 2014 -*Breaking changes* + +### Breaking changes - Classes which include `ActiveRecordModel` must define an `initial_state` class method. -*Fixes* +### Fixed - `ActiveRecordModel.in_state` and `ActiveRecordModel.not_in_state` now handle inital states correctly (patch by [@isaacseymour](https://github.com/isaacseymour)) -*Additions* +### Added - Transition tables created by generated migrations have `NOT NULL` constraints on `to_state`, `sort_key` and foreign key columns (patch by [@greysteil](https://github.com/greysteil)) - `before_transition` and `after_transition` allow an array of to states (patch by [@isaacseymour](https://github.com/isaacseymour)) ## v0.8.3, 2 September 2014 -*Fixes* + +### Fixed - Optimisation for Machine#available_events (patch by [@pacso](https://github.com/pacso)) ## v0.8.2, 2 September 2014 -*Fixes* + +### Fixed - Stop generating a default value for the metadata column if using MySQL. ## v0.8.1, 19 August 2014 -*Fixes* + +### Fixed - Adds check in Machine#transition to make sure the 'to' state is not an empty array (patch by [@barisbalic](https://github.com/barisbalic)) ## v0.8.0, 29 June 2014 -*Additions* + +### Added - Events. Machines can now define events as a logical grouping of transitions (patch by [@iurimatias](https://github.com/iurimatias)) - Retries. Individual transitions can be executed with a retry policy by wrapping the method call in a `Machine.retry_conflicts {}` block (patch by [@greysteil](https://github.com/greysteil)) ## v0.7.0, 25 June 2014 -*Additions* + +### Added - `Adapters::ActiveRecord` now handles `ActiveRecord::RecordNotUnique` errors explicitly and re-raises with a `Statesman::TransitionConflictError` if it is due to duplicate sort_keys (patch by [@greysteil](https://github.com/greysteil)) ## v0.6.1, 21 May 2014 -*Fixes* + +### Fixed + - Fixes an issue where the wrong transition was passed to after_transition callbacks for the second and subsequent transition of a given state machine (patch by [@alan](https://github.com/alan)) ## v0.6.0, 19 May 2014 -*Additions* + +### Added + - Generators now handle namespaced classes (patch by [@hrmrebecca](https://github.com/hrmrebecca)) -*Changes* +### Changed + - `Machine#transition_to` now only swallows Statesman generated errors. An exception in your guard or callback will no longer be caught by Statesman (patch by [@paulspringett](https://github.com/paulspringett)) ## v0.5.0, 27 March 2014 -*Additions* + +### Added + - Scope methods. Adds a module which can be mixed in to an ActiveRecord model to provide `.in_state` and `.not_in_state` query scopes. - Adds `Machine#after_initialize` hook (patch by [@att14](https://github.com/att14)) -*Fixes* +### Fixed + - Added MongoidTransition to the autoload statements, fixing [#29](https://github.com/gocardless/statesman/issues/29) (patch by [@tomclose](https://github.com/tomclose)) ## v0.4.0, 27 February 2014 -*Additions* + +### Added + - Adds after_commit flag to after_transition for callbacks to be executed after the transaction has been committed on the ActiveRecord adapter. These callbacks will still be executed on non transactional adapters. ## v0.3.0, 20 February 2014 -*Additions* + +### Added + - Adds Machine#allowed_transitions method (patch by [@prikha](https://github.com/prikha)) ## v0.2.1, 31 December 2013 -*Fixes* + +### Fixed + - Don't add attr_accessible to generated transition model if running in Rails 4 ## v0.2.0, 16 December 2013 -*Additions* + +### Added + - Adds Ruby 1.9.3 support (patch by [@jakehow](https://github.com/jakehow)) - All Mongo dependent tests are tagged so they can be excluded from test runs -*Changes* +### Changed + - Specs now crash immediately if Mongo is not running ## v0.1.0, 5 November 2013 -*Additions* +### Added + - Adds Mongoid adapter and generators (patch by [@dluxemburg](https://github.com/dluxemburg)) -*Changes* +### Changed + - Replaces `config#transition_class` with `Statesman::Adapters::ActiveRecordTransition` mixin. (inspired by [@cjbell88](https://github.com/cjbell88)) - Renames the active record transition generator from `statesman:transition` to `statesman:active_record_transition`. - Moves to using `require_relative` internally where possible to avoid stomping on application load paths. -## v0.0.1, 28 October 2013. +## v0.0.1, 28 October 2013 + - Initial release diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2ea98b89..5dac9af3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,9 +1,11 @@ +# Contributing + Thanks for taking an interest in contributing to Statesman, here are a few ways you can help make this project better! -## Contributing +## Submitting pull requests -- Generally we welcome new features but please first open an issue where we +- Generally we welcome new features but please first open an issue where we can discuss whether it fits with our vision for the project. - Any new feature or bug fix needs an accompanying test case. - No need to add to the changelog, we will take care of updating it as we make @@ -17,23 +19,22 @@ request passes by running `rubocop`. ## Documentation -Please add a section to the readme for any new feature additions or behaviour -changes. +Please add a section to [the readme](README.md) for any new feature additions or behavioural changes. ## Releasing -We publish new versions of Stateman using [RubyGems](https://guides.rubygems.org/publishing/). Once -the relevant changes have been merged and `VERSION` has been appropriately bumped to the new -version, we run the following command. -``` -$ gem build statesman.gemspec +We publish new versions of Stateman using [RubyGems](https://guides.rubygems.org/publishing/). Once the relevant changes have been merged and `VERSION` has been appropriately bumped to the new version, we run the following command. + +```sh +gem build statesman.gemspec ``` -This builds a `.gem` file locally that will be named something like `statesman-X` where `X` is the -new version. For example, if we are releasing version 9.0.0, the file would be + +This builds a `.gem` file locally that will be named something like `statesman-X` where `X` is the new version. For example, if we are releasing version 9.0.0, the file would be `statesman-9.0.0.gem`. To publish, run `gem push` with the new `.gem` file we just generated. This requires a OTP that is currently only available to GoCardless engineers. For example, if we were to continue to publish version 9.0.0, we would run: -``` -$ gem push statesman-9.0.0.gem + +```sh +gem push statesman-9.0.0.gem ``` diff --git a/README.md b/README.md index 1f4dec75..600e20a3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ +

Statesman

+ A statesmanlike state machine library. @@ -8,7 +10,7 @@ For our policy on compatibility with Ruby and Rails versions, see [COMPATIBILITY [![CircleCI](https://circleci.com/gh/gocardless/statesman.svg?style=shield)](https://circleci.com/gh/gocardless/statesman) [![Code Climate](https://codeclimate.com/github/gocardless/statesman.svg)](https://codeclimate.com/github/gocardless/statesman) [![Gitter](https://badges.gitter.im/join.svg)](https://gitter.im/gocardless/statesman?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![SemVer](https://api.dependabot.com/badges/compatibility_score?dependency-name=statesman&package-manager=bundler&version-scheme=semver)](https://dependabot.com/compatibility-score.html?dependency-name=statesman&package-manager=bundler&version-scheme=semver) +[![SemVer](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=statesman&package-manager=bundler&version-scheme=semver&previous-version=11.0.0&new-version=12.0.0)](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=statesman&package-manager=bundler&version-scheme=semver&previous-version=11.0.0&new-version=12.0.0) Statesman is an opinionated state machine library designed to provide a robust audit trail and data integrity. It decouples the state machine logic from the @@ -16,6 +18,7 @@ underlying model and allows for easy composition with one or more model classes. As such, the design of statesman is a little different from other state machine libraries: + - State behaviour is defined in a separate, "state machine" class, rather than added directly onto a model. State machines are then instantiated with the model to which they should apply. @@ -30,7 +33,7 @@ protection. To get started, just add Statesman to your `Gemfile`, and then run `bundle`: ```ruby -gem 'statesman', '~> 10.0.0' +gem 'statesman', '~> 12.0.0' ``` ## Usage @@ -136,7 +139,7 @@ end class Circular include Statesman::Machine extend Template - + define_states define_transitions end @@ -144,10 +147,10 @@ end class Linear include Statesman::Machine extend Template - + define_states define_transitions - + remove_transitions from: :c, to: :a end @@ -179,7 +182,7 @@ end Generate the transition model: ```bash -$ rails g statesman:active_record_transition Order OrderTransition +rails g statesman:active_record_transition Order OrderTransition ``` Your transition class should @@ -212,13 +215,14 @@ class Order < ActiveRecord::Base :transition_to!, :transition_to, :in_state?, to: :state_machine end ``` -#### Using PostgreSQL JSON column + +### Using PostgreSQL JSON column By default, Statesman uses `serialize` to store the metadata in JSON format. It is also possible to use the PostgreSQL JSON column if you are using Rails 4 or 5. To do that -* Change `metadata` column type in the transition model migration to `json` or `jsonb` +- Change `metadata` column type in the transition model migration to `json` or `jsonb` ```ruby # Before @@ -229,7 +233,7 @@ or 5. To do that t.json :metadata, default: {} ``` -* Remove the `include Statesman::Adapters::ActiveRecordTransition` statement from +- Remove the `include Statesman::Adapters::ActiveRecordTransition` statement from your transition model. (If you want to customise your transition class's "updated timestamp column", as described above, you should define a `.updated_timestamp_column` method on your class and return the name of the column @@ -238,63 +242,73 @@ or 5. To do that ## Configuration -#### `storage_adapter` +### `storage_adapter` ```ruby Statesman.configure do storage_adapter(Statesman::Adapters::ActiveRecord) end ``` + Statesman defaults to storing transitions in memory. If you're using rails, you can instead configure it to persist transitions to the database by using the ActiveRecord adapter. Statesman will fallback to memory unless you specify a transition_class when instantiating your state machine. This allows you to only persist transitions on certain state machines in your app. - ## Class methods -#### `Machine.state` +### `Machine.state` + ```ruby Machine.state(:some_state, initial: true) Machine.state(:another_state) ``` + Define a new state and optionally mark as the initial state. -#### `Machine.transition` +### `Machine.transition` + ```ruby Machine.transition(from: :some_state, to: :another_state) ``` + Define a transition rule. Both method parameters are required, `to` can also be an array of states (`.transition(from: :some_state, to: [:another_state, :some_other_state])`). -#### `Machine.guard_transition` +### `Machine.guard_transition` + ```ruby Machine.guard_transition(from: :some_state, to: :another_state) do |object| object.some_boolean? end ``` + Define a guard. `to` and `from` parameters are optional, a nil parameter means guard all transitions. The passed block should evaluate to a boolean and must be idempotent as it could be called many times. The guard will pass when it evaluates to a truthy value and fail when it evaluates to a falsey value (`nil` or `false`). -#### `Machine.before_transition` +### `Machine.before_transition` + ```ruby Machine.before_transition(from: :some_state, to: :another_state) do |object| object.side_effect end ``` + Define a callback to run before a transition. `to` and `from` parameters are optional, a nil parameter means run before all transitions. This callback can have side-effects as it will only be run once immediately before the transition. -#### `Machine.after_transition` +### `Machine.after_transition` + ```ruby Machine.after_transition(from: :some_state, to: :another_state) do |object, transition| object.side_effect end ``` + Define a callback to run after a successful transition. `to` and `from` parameters are optional, a nil parameter means run after all transitions. The model object and transition object are passed as arguments to the callback. @@ -304,12 +318,14 @@ after the transition. If you specify `after_commit: true`, the callback will be executed once the transition has been committed to the database. -#### `Machine.after_transition_failure` +### `Machine.after_transition_failure` + ```ruby Machine.after_transition_failure(from: :some_state, to: :another_state) do |object, exception| Logger.info("transition to #{exception.to} failed for #{object.id}") end ``` + Define a callback to run if `Statesman::TransitionFailedError` is raised during the execution of transition callbacks. `to` and `from` parameters are optional, a nil parameter means run after all transitions. @@ -318,12 +334,14 @@ This is executed outside of the transaction wrapping other callbacks. If using `transition!` the exception is re-raised after these callbacks are executed. -#### `Machine.after_guard_failure` +### `Machine.after_guard_failure` + ```ruby Machine.after_guard_failure(from: :some_state, to: :another_state) do |object, exception| Logger.info("guard failed during transition to #{exception.to} for #{object.id}") end ``` + Define a callback to run if `Statesman::GuardFailedError` is raised during the execution of guard callbacks. `to` and `from` parameters are optional, a nil parameter means run after all transitions. @@ -332,29 +350,35 @@ This is executed outside of the transaction wrapping other callbacks. If using `transition!` the exception is re-raised after these callbacks are executed. +### `Machine.new` -#### `Machine.new` ```ruby my_machine = Machine.new(my_model, transition_class: MyTransitionModel) ``` + Initialize a new state machine instance. `my_model` is required. If using the ActiveRecord adapter `my_model` should have a `has_many` association with `MyTransitionModel`. -#### `Machine.retry_conflicts` +### `Machine.retry_conflicts` + ```ruby Machine.retry_conflicts { instance.transition_to(:new_state) } ``` + Automatically retry the given block if a `TransitionConflictError` is raised. If you know you want to retry a transition if it fails due to a race condition call it from within this block. Takes an (optional) argument for the maximum number of retry attempts (defaults to 1). -#### `Machine.states` +### `Machine.states` + Returns an array of all possible state names as strings. -#### `Machine.successors` +### `Machine.successors` + Returns a hash of states and the states it is valid for them to transition to. + ```ruby Machine.successors @@ -368,100 +392,129 @@ Machine.successors ## Instance methods -#### `Machine#current_state` +### `Machine#current_state` + Returns the current state based on existing transition objects. Takes an optional keyword argument to force a reload of data from the database. e.g `current_state(force_reload: true)` -#### `Machine#in_state?(:state_1, :state_2, ...)` +### `Machine#in_state?(:state_1, :state_2, ...)` + Returns true if the machine is in any of the given states. -#### `Machine#history` +### `Machine#history` + Returns a sorted array of all transition objects. -#### `Machine#last_transition` +### `Machine#last_transition` + Returns the most recent transition object. -#### `Machine#last_transition_to(:state)` +### `Machine#last_transition_to(:state)` + Returns the most recent transition object to a given state. -#### `Machine#allowed_transitions` +### `Machine#allowed_transitions` + Returns an array of states you can `transition_to` from current state. -#### `Machine#can_transition_to?(:state)` +### `Machine#can_transition_to?(:state)` + Returns true if the current state can transition to the passed state and all applicable guards pass. -#### `Machine#transition_to!(:state)` +### `Machine#transition_to!(:state)` + Transition to the passed state, returning `true` on success. Raises `Statesman::GuardFailedError` or `Statesman::TransitionFailedError` on failure. -#### `Machine#transition_to(:state)` +### `Machine#transition_to(:state)` + Transition to the passed state, returning `true` on success. Swallows all Statesman exceptions and returns false on failure. (NB. if your guard or callback code throws an exception, it will not be caught.) - ## Errors ### Initialization errors + These errors are raised when the Machine and/or Model is initialized. A simple spec like + ```ruby expect { OrderStateMachine.new(Order.new, transition_class: OrderTransition) }.to_not raise_error ``` + will expose these errors as part of your test suite #### InvalidStateError + Raised if: - * Attempting to define a transition without a `to` state. - * Attempting to define a transition with a non-existent state. - * Attempting to define multiple states as `initial`. + +- Attempting to define a transition without a `to` state. +- Attempting to define a transition with a non-existent state. +- Attempting to define multiple states as `initial`. #### InvalidTransitionError + Raised if: - * Attempting to define a callback `from` a state that has no valid transitions (A terminal state). - * Attempting to define a callback `to` the `initial` state if that state has no transitions to it. - * Attempting to define a callback with `from` and `to` where any of the pairs have no transition between them. + +- Attempting to define a callback `from` a state that has no valid transitions (A terminal state). +- Attempting to define a callback `to` the `initial` state if that state has no transitions to it. +- Attempting to define a callback with `from` and `to` where any of the pairs have no transition between them. #### InvalidCallbackError + Raised if: - * Attempting to define a callback without a block. + +- Attempting to define a callback without a block. #### UnserializedMetadataError + Raised if: - * ActiveRecord is configured to not serialize the `metadata` attribute into - to Database column backing it. See the `Using PostgreSQL JSON column` section. + +- ActiveRecord is configured to not serialize the `metadata` attribute into + to Database column backing it. See the `Using PostgreSQL JSON column` section. #### IncompatibleSerializationError + Raised if: - * There is a mismatch between the column type of the `metadata` in the - Database and the model. See the `Using PostgreSQL JSON column` section. + +- There is a mismatch between the column type of the `metadata` in the + Database and the model. See the `Using PostgreSQL JSON column` section. #### MissingTransitionAssociation + Raised if: - * The model that `Statesman::Adapters::ActiveRecordQueries` is included in - does not have a `has_many` association to the `transition_class`. + +- The model that `Statesman::Adapters::ActiveRecordQueries` is included in + does not have a `has_many` association to the `transition_class`. ### Runtime errors + These errors are raised by `transition_to!`. Using `transition_to` will supress `GuardFailedError` and `TransitionFailedError` and return `false` instead. #### GuardFailedError + Raised if: - * A guard callback between `from` and `to` state returned a falsey value. + +- A guard callback between `from` and `to` state returned a falsey value. #### TransitionFailedError + Raised if: - * A transition is attempted but `current_state -> new_state` is not a valid pair. + +- A transition is attempted but `current_state -> new_state` is not a valid pair. #### TransitionConflictError + Raised if: - * A database conflict affecting the `sort_key` or `most_recent` columns occurs - when attempting a transition. - Retried automatically if it occurs wrapped in `retry_conflicts`. +- A database conflict affecting the `sort_key` or `most_recent` columns occurs + when attempting a transition. + Retried automatically if it occurs wrapped in `retry_conflicts`. ## Model scopes @@ -498,14 +551,16 @@ class Order < ActiveRecord::Base end ``` -#### `Model.in_state(:state_1, :state_2, etc)` +### `Model.in_state(:state_1, :state_2, etc)` + Returns all models currently in any of the supplied states. -#### `Model.not_in_state(:state_1, :state_2, etc)` +### `Model.not_in_state(:state_1, :state_2, etc)` + Returns all models not currently in any of the supplied states. +### `Model.most_recent_transition_join` -#### `Model.most_recent_transition_join` This joins the model to its most recent transition whatever that may be. We expose this method to ease use of ActiveRecord's `or` e.g @@ -517,7 +572,7 @@ Model.in_state(:state_1).or( ## Frequently Asked Questions -#### Storing the state on the model object +### Storing the state on the model object If you wish to store the model state on the model directly, you can keep it up to date using an `after_transition` hook. @@ -533,7 +588,7 @@ end You could also use a calculated column or view in your database. -#### Accessing metadata from the last transition +### Accessing metadata from the last transition Given a field `foo` that was stored in the metadata, you can access it like so: @@ -541,7 +596,7 @@ Given a field `foo` that was stored in the metadata, you can access it like so: model_instance.state_machine.last_transition.metadata["foo"] ``` -#### Events +### Events Used to using a state machine with "events"? Support for events is provided by the [statesman-events](https://github.com/gocardless/statesman-events) gem. Once @@ -557,31 +612,34 @@ class OrderStateMachine end ``` -#### Deleting records. +### Deleting records If you need to delete the Parent model regularly you will need to change either the association deletion behaviour or add a `DELETE CASCADE` condition to foreign key in your database. E.g -``` + +```ruby has_many :order_transitions, autosave: false, dependent: :destroy ``` + or when migrating the transition model -``` + +```ruby add_foreign_key :order_transitions, :orders, on_delete: :cascade ``` - ## Testing Statesman Implementations This answer was abstracted from [this issue](https://github.com/gocardless/statesman/issues/77). At GoCardless we focus on testing that: + - guards correctly prevent / allow transitions - callbacks execute when expected and perform the expected actions -#### Testing Guards +### Testing Guards Guards can be tested by asserting that `transition_to!` does or does not raise a `Statesman::GuardFailedError`: @@ -597,7 +655,7 @@ describe "guards" do end ``` -#### Testing Callbacks +### Testing Callbacks Callbacks are tested by asserting that the action they perform occurs: From decae2cd1d297d09f160a8b8836a1ce237df4409 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 5 Jan 2024 11:25:06 +0000 Subject: [PATCH 22/26] Fix lint warning in COMPATIBILILTY.md --- docs/COMPATIBILITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md index ae224b37..773d44aa 100644 --- a/docs/COMPATIBILITY.md +++ b/docs/COMPATIBILITY.md @@ -8,7 +8,7 @@ To that end, [our build matrix](../.circleci/config.yml) includes all these vers Any time Statesman doesn't work on a supported combination of Ruby and Rails, it's a bug, and can be reported [here](https://github.com/gocardless/statesman/issues). -# Deprecation +## Deprecation Whenever a version of Ruby or Rails falls out of support, we will mirror that change in Statesman by updating the build matrix and releasing a new major version. From 8086957f23c0a0d0444ea90972d94855b26f0da9 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 5 Jan 2024 11:26:56 +0000 Subject: [PATCH 23/26] Update build matrix link in COMPATIBILITY.md --- docs/COMPATIBILITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md index 773d44aa..fd76e626 100644 --- a/docs/COMPATIBILITY.md +++ b/docs/COMPATIBILITY.md @@ -4,7 +4,7 @@ Our goal as Statesman maintainers is for the library to be compatible with all s Specifically, any CRuby/MRI version that has not received an End of Life notice ([e.g. this notice for Ruby 2.1](https://www.ruby-lang.org/en/news/2017/04/01/support-of-ruby-2-1-has-ended/)) is supported. Similarly, any version of Rails listed as currently supported on [this page](http://guides.rubyonrails.org/maintenance_policy.html) is one we aim to support in Statesman. -To that end, [our build matrix](../.circleci/config.yml) includes all these versions. +To that end, [our build matrix](../.github/workflows/tests.yml) includes all these versions. Any time Statesman doesn't work on a supported combination of Ruby and Rails, it's a bug, and can be reported [here](https://github.com/gocardless/statesman/issues). From 7640a2ab47f8a2399abbad58bc96f722d3722458 Mon Sep 17 00:00:00 2001 From: Tabitha Cromarty Date: Fri, 5 Jan 2024 11:48:53 +0000 Subject: [PATCH 24/26] Add Ruby 3.3 to build matrix (#533) Also update lingering references to Ruby 2.7 in rubocop & gemspec config as it has been EOL for a while now. --- .github/workflows/tests.yml | 10 +++++++--- .rubocop.yml | 2 +- .ruby-version | 2 +- statesman.gemspec | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3cc0fd7b..625dc6f4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - ruby-version: ["3.0", "3.1", "3.2"] + ruby-version: ["3.0", "3.1", "3.2", "3.3"] rails-version: - "6.1.7.6" - "7.0.8" @@ -34,6 +34,8 @@ jobs: exclude: - ruby-version: "3.2" rails-version: "6.1.7.6" + - ruby-version: "3.3" + rails-version: "6.1.7.6" runs-on: ubuntu-latest services: postgres: @@ -67,7 +69,7 @@ jobs: strategy: fail-fast: false matrix: - ruby-version: ["3.0", "3.1", "3.2"] + ruby-version: ["3.0", "3.1", "3.2", "3.3"] rails-version: - "6.1.7.6" - "7.0.8" @@ -75,7 +77,9 @@ jobs: - "main" mysql-version: ["8.0", "8.2"] exclude: - - ruby-version: 3.2 + - ruby-version: "3.2" + rails-version: "6.1.7.6" + - ruby-version: "3.3" rails-version: "6.1.7.6" runs-on: ubuntu-latest services: diff --git a/.rubocop.yml b/.rubocop.yml index 96d99866..84904ecf 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -4,7 +4,7 @@ inherit_gem: gc_ruboconfig: rubocop.yml AllCops: - TargetRubyVersion: 2.7 + TargetRubyVersion: 3.0 NewCops: enable Metrics/AbcSize: diff --git a/.ruby-version b/.ruby-version index be94e6f5..15a27998 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.2.2 +3.3.0 diff --git a/statesman.gemspec b/statesman.gemspec index 2fc72ed7..6a78dd0a 100644 --- a/statesman.gemspec +++ b/statesman.gemspec @@ -20,7 +20,7 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - spec.required_ruby_version = ">= 2.7" + spec.required_ruby_version = ">= 3.0" spec.add_development_dependency "ammeter", "~> 1.1" spec.add_development_dependency "bundler", "~> 2" From 9fb3317607bb6df8bed63d76f12ee42e61ea5535 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Jan 2024 13:48:31 +0000 Subject: [PATCH 25/26] Update gc_ruboconfig requirement from ~> 4.3.0 to ~> 4.4.1 (#526) Updates the requirements on [gc_ruboconfig](https://github.com/gocardless/ruboconfig) to permit the latest version. - [Release notes](https://github.com/gocardless/ruboconfig/releases) - [Changelog](https://github.com/gocardless/gc_ruboconfig/blob/master/CHANGELOG.md) - [Commits](https://github.com/gocardless/ruboconfig/compare/v4.3.0...v4.4.1) --- updated-dependencies: - dependency-name: gc_ruboconfig dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- statesman.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/statesman.gemspec b/statesman.gemspec index 6a78dd0a..15532ead 100644 --- a/statesman.gemspec +++ b/statesman.gemspec @@ -24,7 +24,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency "ammeter", "~> 1.1" spec.add_development_dependency "bundler", "~> 2" - spec.add_development_dependency "gc_ruboconfig", "~> 4.3.0" + spec.add_development_dependency "gc_ruboconfig", "~> 4.4.1" spec.add_development_dependency "mysql2", ">= 0.4", "< 0.6" spec.add_development_dependency "pg", ">= 0.18", "<= 1.6" spec.add_development_dependency "rails", ">= 5.2" From 30be3210dc16894c85eeec9fa68f4c421c864616 Mon Sep 17 00:00:00 2001 From: Stephen Binns Date: Fri, 5 Jan 2024 15:13:10 +0000 Subject: [PATCH 26/26] Added release notes for 12.1.0 --- CHANGELOG.md | 13 +++++++++++++ lib/statesman/version.rb | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa2bb82c..588840de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## v12.1.0 5th January 2024 + +### Fixed + +- Fixed autoloading the VERSION constants +- Fixed Ensuring inheritance issues with STI tabled +- Enabled gaplock protection when using trilogy mysql adapter + +### Added + +- Added Ruby 3.3 to build matrix +- Added optional initial transition + ## v12.0.0 30th November 2023 ### Added diff --git a/lib/statesman/version.rb b/lib/statesman/version.rb index 45813dcd..61cd55c1 100644 --- a/lib/statesman/version.rb +++ b/lib/statesman/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Statesman - VERSION = "12.0.0" + VERSION = "12.1.0" end