Stateful
Description
A Ruby state machine which allows you to easily add state to Poro, ActiveRecord, and Sequel objects. Works with both CRuby and mRuby.
Installation
CRuby
Add this line to your application's Gemfile:
gem 'stateful.rb'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install stateful.rb
mRuby
Add this to your build_config.rb:
conf.gem github: 'thoran/Stateful'
Usage
1. Poro with persistence-specific extend and a stateful block declaration
class PoroMachine
extend Stateful::Poro
stateful do
initial_state :initial_state
state :initial_state do
on :an_event => :next_state
on :another_event => :final_state
end
state :next_state do
on :yet_another_event => :final_state
end
final_state :final_state
end
end
poro_machine = PoroMachine.new
poro_machine.initial_state.name
# => :initial_state
poro_machine.final_state.name
# => :final_state
2. Poro with persistence non-specific extend, without a stateful block declaration, an initial_state block, and multiple final states
class PoroMachine
extend Stateful
initial_state :initial_state do
on :an_event => :next_state
on :another_event => :final_state0
end
state :next_state do
on :yet_another_event => :final_state1
end
final_state :final_state0, :final_state1
end
poro_machine = PoroMachine.new
poro_machine.current_state.name
# => :initial_state
poro_machine.an_event
poro_machine.current_state.name
# => :next_state
3. ActiveRecord with persistence-specific extend and a stateful block declaration
class ActiveRecordMachine < ActiveRecord::Base
extend Stateful::ActiveRecord
stateful do
initial_state :initial_state
state :initial_state do
on :an_event => :next_state
on :another_event => :final_state
end
state :next_state do
on :yet_another_event => :final_state
end
final_state :final_state
end
end
active_record_machine = ActiveRecordMachine.new
active_record_machine.initial_state?
# => true
active_record_machine.an_event
active_record_machine.next_state?
# => true
4. ActiveRecord with persistence non-specific extend, without a stateful block declaration, an initial_state block, and multiple final states
class ActiveRecordMachine < ActiveRecord::Base
extend Stateful
initial_state :initial_state do
on :an_event => :next_state
on :another_event => :final_state0
end
state :next_state do
on :yet_another_event => :final_state0
on :and_yet_another_event => :final_state1
end
final_state :final_state0, :final_state1
end
active_record_machine = ActiveRecordMachine.new
active_record_machine.an_event
active_record_machine.final_state?
# => false
active_record_machine.yet_another_event
active_record_machine.final_state?
# => true
5. Sequel with persistence-specific extend and a stateful block declaration
class SequelMachine < Sequel::Model
extend Stateful::Sequel
stateful do
initial_state :initial_state
state :initial_state do
on :an_event => :next_state
on :another_event => :final_state
end
state :next_state do
on :yet_another_event => :final_state
end
final_state :final_state
end
end
sequel_machine = SequelMachine.create
sequel_machine.initial_state?
# => true
sequel_machine.an_event
sequel_machine.next_state?
# => true
6. Sequel with persistence non-specific extend, without a stateful block declaration, an initial_state block, and multiple final states
class SequelMachine < Sequel::Model
extend Stateful
initial_state :initial_state do
on :an_event => :next_state
on :another_event => :final_state0
end
state :next_state do
on :yet_another_event => :final_state0
on :and_yet_another_event => :final_state1
end
final_state :final_state0, :final_state1
end
sequel_machine = SequelMachine.create
sequel_machine.an_event
sequel_machine.final_state?
# => false
sequel_machine.yet_another_event
sequel_machine.final_state?
# => true
7. Custom attribute name and non-deterministic event ordering
It's also possible to define a state machine which has a custom attribute name, which fires off the events in a random or non-deterministic order, and which has no final state.
class ActiveRecordMachine < ActiveRecord::Base
@stateful_column_name = 'status' # This needs to be set before the class is extended. The default is 'current_state'.
extend Stateful
initial_state :initial_state, non_deterministic: true do
on :an_event => :another_state
on :another_event => :yet_another_state
end
state :another_state do
on :another_event => :yet_another_state
end
state :yet_another_state do
on :yet_another_event => :another_state
end
end
active_record_machine = ActiveRecordMachine.new
# Supposing that both :an_event or :another_event messages are able to fire, then if the order of the evaluation of the transitions is non-deterministic, then the state change is also. Non-deterministic event ordering really only makes sense in the context of the sibling library thoran/Eventful, which will evaluate the transitions automatically and will do so in the order as presented by Stateful.
active_record_machine.another_event # With non-deterministic event firing it could check for :another_event before checking :an_event.
active_record_machine.current_state.name
# => :yet_another_state
active_record_machine.an_event
active_record_machine.current_state.name
# => :yet_another_state
active_record_machine.final_state?
# => false
active_record_machine.yet_another_event
active_record_machine.final_state?
# => false
6. Multiple state machines per class
A single class can have multiple independent (orthogonal) state machines. Each machine gets its own namespaced methods, while domain predicates and event methods remain unnamespaced.
class Order
extend Stateful
state_machine :fulfilment do
initial_state :pending do
on :items_collected => :ready
end
state :ready do
on :dispatch => :dispatched
end
final_state :dispatched
end
state_machine :payment do
initial_state :unpaid do
on :pay => :paid
end
state :paid do
on :refund => :refunded
end
final_state :refunded
end
end
order = Order.new
order.fulfilment_state.name # state accessors are namespaced.
# => :pending
order.payment_state.name
# => :unpaid
order.fulfilment_initial_state? # General/overlapping predicates are namespaced.
# => true
order.payment_final_state?
# => false
order.fulfilment_transitions.first.event_name # Transitions are namespaced.
# => :items_collected
order.pending? # Domain predicates are not namespaced.
# => true
order.unpaid?
# => true
order.pay # Event methods are not namespaced.
order.paid?
# => true
# State machines are orthogonal
order.fulfilment_initial_state?
# => true
Multiple state machines can also be declared inside a stateful block:
class Order
extend Stateful
stateful do
state_machine :fulfilment do
initial_state :pending do
on :items_collected => :ready
end
state :ready do
on :dispatch => :dispatched
end
final_state :dispatched
end
state_machine :payment do
initial_state :unpaid do
on :pay => :paid
end
state :paid do
on :refund => :refunded
end
final_state :refunded
end
end
end
For ActiveRecord and Sequel, each machine uses a column named <machine_name>_state:
class CreateOrders < ActiveRecord::Migration[6.0]
def change
create_table :orders do |t|
t.string :fulfilment_state
t.string :payment_state
end
end
end
For Poro, each machine uses an instance variable named @<machine_name>_state.
Contributing
- Fork it: https://github.com/thoran/Stateful/fork
- Create your feature branch:
git checkout -b my-new-feature - Commit your changes:
git commit -am 'Add some feature' - Push to the branch:
git push origin my-new-feature - Create a new pull request