Skip to content

Commit

Permalink
Introduce on_success callbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
getand committed Mar 26, 2020
1 parent 7c2cfc0 commit 2a590f0
Show file tree
Hide file tree
Showing 10 changed files with 115 additions and 14 deletions.
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.


Expand All @@ -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
```

Expand Down
1 change: 1 addition & 0 deletions lib/nxt_state_machine/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion lib/nxt_state_machine/integrations/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions lib/nxt_state_machine/integrations/attr_accessor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions lib/nxt_state_machine/integrations/hash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
11 changes: 11 additions & 0 deletions lib/nxt_state_machine/state_machine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
11 changes: 8 additions & 3 deletions lib/nxt_state_machine/transition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
2 changes: 1 addition & 1 deletion lib/nxt_state_machine/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module NxtStateMachine
VERSION = "0.1.6"
VERSION = "0.1.7"
end
75 changes: 75 additions & 0 deletions spec/integrations/active_record_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 2a590f0

Please sign in to comment.