From 8b6a779e5683dcdef18f2e082f261c581e2a9d32 Mon Sep 17 00:00:00 2001 From: Abdelkader Boudih Date: Sun, 29 Jun 2025 18:04:44 +0100 Subject: [PATCH] feat: modernize ActiveModel integration with backward compatibility avoid breaking reality for apps that don't update the syntax --- .../integrations/active_model.rb | 16 ++++- ...chine_initialization_compatibility_test.rb | 60 +++++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 test/machine_initialization_compatibility_test.rb diff --git a/lib/state_machines/integrations/active_model.rb b/lib/state_machines/integrations/active_model.rb index f912d04..a75f970 100644 --- a/lib/state_machines/integrations/active_model.rb +++ b/lib/state_machines/integrations/active_model.rb @@ -353,12 +353,22 @@ def around_validation(object) def define_state_initializer define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1 - def initialize(**params) - params.transform_keys! do |key| + def initialize(params = nil, **kwargs) + # Support both positional hash and keyword arguments + attrs = params.nil? ? kwargs : params + #{' '} + attrs.transform_keys! do |key| self.class.attribute_aliases[key.to_s] || key.to_s end if self.class.respond_to?(:attribute_aliases) - self.class.state_machines.initialize_states(self, {}, params) { super } + # Call super with the appropriate arguments based on what we received + self.class.state_machines.initialize_states(self, {}, attrs) do + if params + super(params) + else + super(**kwargs) + end + end end end_eval end diff --git a/test/machine_initialization_compatibility_test.rb b/test/machine_initialization_compatibility_test.rb new file mode 100644 index 0000000..8a92971 --- /dev/null +++ b/test/machine_initialization_compatibility_test.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require_relative 'test_helper' + +class MachineInitializationCompatibilityTest < BaseTestCase + def setup + @model = new_model do + include ActiveModel::Validations + attr_accessor :state + end + + @machine = StateMachines::Machine.new(@model, initial: :parked) + @machine.state :parked, :idling + @machine.event :ignite + end + + def test_should_accept_positional_hash_argument + record = @model.new({ state: 'idling' }) + assert_equal 'idling', record.state + end + + def test_should_accept_keyword_arguments + record = @model.new(state: 'idling') + assert_equal 'idling', record.state + end + + def test_should_accept_empty_initialization + record = @model.new + assert_equal 'parked', record.state + end + + def test_should_handle_attribute_aliases + @model.class_eval do + def self.attribute_aliases + { 'status' => 'state' } + end + end + + record = @model.new(status: 'idling') + assert_equal 'idling', record.state + end + + def test_should_prefer_positional_hash_over_keywords_when_both_present + # If someone accidentally provides both, positional takes precedence + record = @model.new({ state: 'idling' }, state: 'parked') + assert_equal 'idling', record.state + end + + def test_should_handle_empty_positional_hash + # Empty hash should still be treated as positional argument + record = @model.new({}) + assert_equal 'parked', record.state # Gets default initial state + end + + def test_should_use_keywords_when_empty_hash_and_keywords_present + # With the fix, keywords are ignored even with empty positional hash + record = @model.new({}, state: 'idling') + assert_equal 'parked', record.state # Empty hash takes precedence + end +end