diff --git a/Gemfile.lock b/Gemfile.lock index 21bc441..f38682f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - nxt_state_machine (0.1.6) + nxt_state_machine (0.1.7) activesupport nxt_registry (~> 0.1.3) diff --git a/README.md b/README.md index 30c10a0..d15f03b 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,10 @@ class ArticleWorkflow puts 'around transition exit' end + on_success from: any_state, to: :approved do |transition| + # This is the last callback in the chain - It runs outside of the active record transaction + end + on_error CustomError from: any_state, to: :approved do |error, transition| end end @@ -277,10 +281,10 @@ Transitions can be halted in callbacks and during the transition itself simply b ### Callbacks -You can register `before_transition`, `around_transition` and `after_transition` callbacks. By defining the -:from and :to states you decide on which transitions the callback actually runs. Around callbacks need to call the -proc object that they get passed in. Registering callbacks inside an event block or on the state_machine top level -behaves exactly the same way and is only a matter of structure. The only thing that defines when callbacks run is +You can register `before_transition`, `around_transition`, `after_transition` and `on_success` callbacks. +By defining the :from and :to states you decide on which transitions the callback actually runs. Around callbacks need +to call the proc object that they get passed in. Registering callbacks inside an event block or on the state_machine top +level behaves exactly the same way and is only a matter of structure. The only thing that defines when callbacks run is the :from and :to parameters with which they are registered. @@ -298,6 +302,11 @@ event :approve do block.call puts 'around transition exit' end + + # Use this to trigger another event after the transaction around the transition completed + on_success from: any_state, to: :approved do |transition| + # This is the last callback in the chain - It runs outside of the active record transaction + end end ``` diff --git a/lib/nxt_state_machine/event.rb b/lib/nxt_state_machine/event.rb index d8f9dd1..33c76c8 100644 --- a/lib/nxt_state_machine/event.rb +++ b/lib/nxt_state_machine/event.rb @@ -18,6 +18,7 @@ def initialize(name, state_machine, **options, &block) delegate :before_transition, :after_transition, :around_transition, + :on_success, :on_error, :on_error!, :any_state, diff --git a/lib/nxt_state_machine/integrations/active_record.rb b/lib/nxt_state_machine/integrations/active_record.rb index f344d4f..c4763c3 100644 --- a/lib/nxt_state_machine/integrations/active_record.rb +++ b/lib/nxt_state_machine/integrations/active_record.rb @@ -65,7 +65,7 @@ def set_state(machine, target, transition, state_attr, save_with_method) raise defused_error if defused_error - result + transition.run_success_callbacks || result rescue StandardError => error target.assign_attributes(state_attr => transition.from.to_s) diff --git a/lib/nxt_state_machine/integrations/attr_accessor.rb b/lib/nxt_state_machine/integrations/attr_accessor.rb index 957bc13..d68d76b 100644 --- a/lib/nxt_state_machine/integrations/attr_accessor.rb +++ b/lib/nxt_state_machine/integrations/attr_accessor.rb @@ -22,7 +22,7 @@ def state_machine(name = :default, state_attr: :state, target: nil, &config) result = set_state(target, transition, state_attr) transition.run_after_callbacks - result + transition.run_success_callbacks || result rescue StandardError => error target.send("#{state_attr}=", transition.from.enum) @@ -38,7 +38,7 @@ def state_machine(name = :default, state_attr: :state, target: nil, &config) result = set_state(target, transition, state_attr) transition.run_after_callbacks - result + transition.run_success_callbacks || result rescue StandardError target.send("#{state_attr}=", transition.from.enum) raise diff --git a/lib/nxt_state_machine/integrations/hash.rb b/lib/nxt_state_machine/integrations/hash.rb index 5ed9bb8..86a3174 100644 --- a/lib/nxt_state_machine/integrations/hash.rb +++ b/lib/nxt_state_machine/integrations/hash.rb @@ -21,7 +21,7 @@ def state_machine(name = :default, state_attr: :state, target: nil, &config) transition.run_before_callbacks result = set_state(current_target, transition, state_attr) transition.run_after_callbacks - result + transition.run_success_callbacks || result rescue StandardError => error current_target[state_attr] = transition.from.enum @@ -37,7 +37,7 @@ def state_machine(name = :default, state_attr: :state, target: nil, &config) result = set_state(current_target, transition, state_attr) transition.run_after_callbacks - result + transition.run_success_callbacks || result rescue StandardError current_target[state_attr] = transition.from.enum raise diff --git a/lib/nxt_state_machine/state_machine.rb b/lib/nxt_state_machine/state_machine.rb index 58dbd74..91ab8b0 100644 --- a/lib/nxt_state_machine/state_machine.rb +++ b/lib/nxt_state_machine/state_machine.rb @@ -117,6 +117,10 @@ def after_transition(from:, to:, run: nil, &block) callbacks.register(from, to, :after, run, block) end + def on_success(from:, to:, run: nil, &block) + callbacks.register(from, to, :success, run, block) + end + def defuse(errors = [], from:, to:) defuse_registry.register(from, to, errors) end @@ -147,16 +151,23 @@ def run_after_callbacks(transition, context) run_callbacks(transition, :after, context) end + def run_success_callbacks(transition, context) + run_callbacks(transition, :success, context) + end + def find_error_callback(error, transition) error_callback_registry.resolve(error, transition) end def run_callbacks(transition, kind, context) current_callbacks = callbacks.resolve(transition, kind) + return unless current_callbacks.any? current_callbacks.each do |callback| Callable.new(callback).bind(context).call(transition) end + + true end def current_state_name(context) diff --git a/lib/nxt_state_machine/transition.rb b/lib/nxt_state_machine/transition.rb index 5c3850a..edfedfb 100644 --- a/lib/nxt_state_machine/transition.rb +++ b/lib/nxt_state_machine/transition.rb @@ -11,9 +11,10 @@ def initialize(name, event:, from:, to:, state_machine:, context:, set_state_met @set_state_method = set_state_method @context = context @block = block + @result = nil end - attr_reader :name, :from, :to, :block, :event + attr_reader :name, :from, :to, :block, :event, :result # This triggers the set state method def trigger @@ -31,7 +32,7 @@ def trigger # This must be used in set_state method to actually execute the transition within the around callback chain def execute(&block) - Transition::Proxy.new(event, state_machine,self, context).call(&block) + self.result = Transition::Proxy.new(event, state_machine,self, context).call(&block) end alias_method :with_around_callbacks, :execute @@ -44,9 +45,13 @@ def run_after_callbacks state_machine.run_after_callbacks(self, context) end + def run_success_callbacks + state_machine.run_success_callbacks(self, context) + end + private attr_reader :state_machine, :set_state_method, :context - attr_writer :block + attr_writer :block, :result end end diff --git a/lib/nxt_state_machine/version.rb b/lib/nxt_state_machine/version.rb index d64e0a4..067e03d 100644 --- a/lib/nxt_state_machine/version.rb +++ b/lib/nxt_state_machine/version.rb @@ -1,3 +1,3 @@ module NxtStateMachine - VERSION = "0.1.6" + VERSION = "0.1.7" end diff --git a/spec/integrations/active_record_spec.rb b/spec/integrations/active_record_spec.rb index 7656566..27d3fa5 100644 --- a/spec/integrations/active_record_spec.rb +++ b/spec/integrations/active_record_spec.rb @@ -579,6 +579,81 @@ def append_result(tmp) end end end + + context 'success callbacks' do + let(:state_machine_class) do + Class.new do + include NxtStateMachine::ActiveRecord + + def initialize(application) + @application = application + end + + attr_reader :application + + state_machine(state_attr: :status, target: :application) do + state :received, initial: true + state :processed, :accepted, :rejected + + event :process do + transitions from: :received, to: :processed do |raise_in: ''| + application.processed_at = Time.current + raise_in + end + + after_transition from: :received, to: :processed do |transition| + raise ZeroDivisionError, "After transition" if transition.result == 'raise_in_after_transition' + end + + on_success from: :received, to: :processed do |transition| + raise ZeroDivisionError, "On success" if transition.result == 'raise_in_on_success' + accept! + end + end + + event :accept do + transitions from: :processed, to: :accepted do + application.accepted_at = Time.current + end + end + end + end + end + + let(:application) { + Application.create!( + content: 'Please make it happen', + received_at: Time.current, + status: 'received' + ) + } + + subject do + state_machine_class.new(application) + end + + context 'when there is an error before' do + it 'does not run the on success callback' do + expect { subject.process!(raise_in: 'raise_in_after_transition') }.to raise_error(ZeroDivisionError, /After transition/) + expect(application.reload.status).to eq('received') + end + end + + context 'when there is no error before' do + context 'when there is an error in the success callback' do + it do + expect { subject.process!(raise_in: 'raise_in_on_success') }.to raise_error(ZeroDivisionError, /On success/) + expect(application.reload.status).to eq('processed') + end + end + + context 'when triggering another transition' do + it 'transitions to the next state' do + expect { subject.process! }.to change { application.reload.status }.from('received').to('accepted') + end + end + end + end end end