README.rdoc in state_machine-0.3.1 vs README.rdoc in state_machine-0.4.0

- old
+ new

@@ -1,9 +1,9 @@ == state_machine -+state_machine+ adds support for creating state machines for attributes within -a model. ++state_machine+ adds support for creating state machines for attributes on any +Ruby class. == Resources API @@ -21,36 +21,53 @@ * git://github.com/pluginaweek/state_machine.git == Description -State machines make it dead-simple to manage the behavior of a model. Too often, -the status of a record is kept by creating multiple boolean columns in the table -and deciding how to behave based on the values in those columns. This can become -cumbersome and difficult to maintain when the complexity of your models starts to -increase. +State machines make it dead-simple to manage the behavior of a class. Too often, +the status of an object is kept by creating multiple boolean attributes and +deciding how to behave based on the values. This can become cumbersome and +difficult to maintain when the complexity of your class starts to increase. +state_machine+ simplifies this design by introducing the various parts of a real -state machine, including states, events, and transitions. However, the api is -designed to be similar to ActiveRecord in terms of validations and callbacks, -making it so simple you don't even need to know what a state machine is :) +state machine, including states, events, transitions, and callbacks. However, +the api is designed to be so simple you don't even need to know what a +state machine is :) +Some brief, high-level features include: +* Defining state machines on any Ruby class +* Multiple state machines on a single class +* before/after transition hooks with explicit transition requirements +* ActiveRecord integration +* DataMapper integration +* Sequel integration +* States of any data type +* State predicates +* GraphViz visualization creator + +Examples of the usage patterns for some of the above features are shown below. +You can find more detailed documentation in the actual API. + == Usage === Example Below is an example of many of the features offered by this plugin, including * Initial states * Transition callbacks * Conditional transitions - class Vehicle < ActiveRecord::Base + class Vehicle + attr_accessor :seatbelt_on + state_machine :state, :initial => 'parked' do before_transition :from => %w(parked idling), :do => :put_on_seatbelt - after_transition :to => 'parked', :do => lambda {|vehicle| vehicle.update_attribute(:seatbelt_on, false)} - after_transition :on => 'crash', :do => :tow! - after_transition :on => 'repair', :do => :fix! + after_transition :on => 'crash', :do => :tow + after_transition :on => 'repair', :do => :fix + after_transition :to => 'parked' do |vehicle, transition| + vehicle.seatbelt_on = false + end event :park do transition :to => 'parked', :from => %w(idling first_gear) end @@ -81,48 +98,116 @@ event :repair do transition :to => 'parked', :from => 'stalled', :if => :auto_shop_busy? end end - def tow! - # do something here + def initialize + @seatbelt_on = false end - def fix! - # do something here + def put_on_seatbelt + @seatbelt_on = true end def auto_shop_busy? false end + + def tow + # tow the vehicle + end + + def fix + # get the vehicle fixed by a mechanic + end end -Using the above model as an example, you can interact with the state machine +Using the above class as an example, you can interact with the state machine like so: - vehicle = Vehicle.create # => #<Vehicle id: 1, seatbelt_on: false, state: "parked"> - vehicle.ignite # => true - vehicle # => #<Vehicle id: 1, seatbelt_on: true, state: "idling"> - vehicle.shift_up # => true - vehicle # => #<Vehicle id: 1, seatbelt_on: true, state: "first_gear"> - vehicle.shift_up # => true - vehicle # => #<Vehicle id: 1, seatbelt_on: true, state: "second_gear"> + vehicle = Vehicle.new # => #<Vehicle:0xb7cf4eac @state="parked", @seatbelt_on=false> + vehicle.parked? # => true + vehicle.can_ignite? # => true + vehicle.next_ignite_transition # => #<StateMachine::Transition:0xb7c34cec ...> + vehicle.ignite # => true + vehicle.parked? # => false + vehicle.idling? # => true + vehicle # => #<Vehicle:0xb7cf4eac @state="idling", @seatbelt_on=true> + vehicle.shift_up # => true + vehicle # => #<Vehicle:0xb7cf4eac @state="first_gear", @seatbelt_on=true> + vehicle.shift_up # => true + vehicle # => #<Vehicle:0xb7cf4eac @state="second_gear", @seatbelt_on=true> # The bang (!) operator can raise exceptions if the event fails - vehicle.park! # => PluginAWeek::StateMachine::InvalidTransition: Cannot transition via :park from "second_gear" + vehicle.park! # => StateMachine::InvalidTransition: Cannot transition via :park from "second_gear" + + # Generic state predicates can raise exceptions if the value does not exist + vehicle.state?('parked') # => true + vehicle.state?('invalid') # => ArgumentError: "parked" is not a known state value -=== With enumerations +== Integrations +In addition to being able to define state machines on all Ruby classes, a set of +out-of-the-box integrations are available for some of the more popular Ruby +libraries. These integrations add library-specific behavior, allowing for state +machines to work more tightly with the conventions defined by those libraries. + +The integrations currently available include: +* ActiveRecord models +* DataMapper resources +* Sequel models + +A brief overview of these integrations is described below. + +=== ActiveRecord + +The ActiveRecord integration adds support for database transactions, automatically +saving the record, named scopes, and observers. For example, + + class Vehicle < ActiveRecord::Base + state_machine :initial => 'parked' do + before_transition :to => 'idling', :do => :put_on_seatbelt + after_transition :to => 'parked' do |vehicle, transition| + vehicle.seatbelt = 'off' + end + + event :ignite do + transition :to => 'idling', :from => 'parked' + end + end + + def put_on_seatbelt + ... + end + end + + class VehicleObserver < ActiveRecord::Observer + # Callback for :ignite event *before* the transition is performed + def before_ignite(vehicle, transition) + # log message + end + + # Generic transition callback *before* the transition is performed + def after_transition(vehicle, transition) + Audit.log(vehicle, transition) + end + end + +For more information about the various behaviors added for ActiveRecord state +machines, see StateMachine::Integrations::ActiveRecord. + +==== With enumerations + Using the acts_as_enumeration[http://github.com/pluginaweek/acts_as_enumeration] plugin -states can be transparently stored using record ids in the database like so: +with an ActiveRecord integration, states can be transparently stored using +record ids in the database like so: class VehicleState < ActiveRecord::Base acts_as_enumeration create :id => 1, :name => 'parked' create :id => 2, :name => 'idling' - create :id => 3, :name => 'first_gear' ... end class Vehicle < ActiveRecord::Base belongs_to :state, :class_name => 'VehicleState' @@ -131,51 +216,174 @@ ... event :park do transition :to => 'parked', :from => %w(idling first_gear) end - - event :ignite do - transition :to => 'stalled', :from => 'stalled' - transition :to => 'idling', :from => 'parked' - end end ... end -Notice in the above example, the state machine definition remains *exactly* the -same. However, when interacting with the records, the actual state will be -stored using the identifiers defined for the enumeration: +Notice that the state machine definition remains *exactly* the same. However, +when interacting with the records, the actual state will be stored using the +identifiers defined for the enumeration: vehicle = Vehicle.create # => #<Vehicle id: 1, seatbelt_on: false, state_id: 1> vehicle.ignite # => true vehicle # => #<Vehicle id: 1, seatbelt_on: true, state_id: 2> - vehicle.shift_up # => true - vehicle # => #<Vehicle id: 1, seatbelt_on: true, state_id: 3> This allows states to take on more complex functionality other than just being a string value. +=== DataMapper + +Like the ActiveRecord integration, the DataMapper integration adds support for +database transactions, automatically saving the record, named scopes, Extlib-like +callbacks, and observers. For example, + + class Vehicle + include DataMapper::Resource + + property :id, Serial + property :state, String + + state_machine :initial => 'parked' do + before_transition :to => 'idling', :do => :put_on_seatbelt + after_transition :to => 'parked' do |transition| + self.seatbelt = 'off' # self is the record + end + + event :ignite do + transition :to => 'idling', :from => 'parked' + end + end + + def put_on_seatbelt + ... + end + end + + class VehicleObserver + include DataMapper::Observer + + observe Vehicle + + # Callback for :ignite event *before* the transition is performed + before_transition :on => :ignite do |transition| + # log message (self is the record) + end + + # Generic transition callback *before* the transition is performed + after_transition do |transition, saved| + Audit.log(self, transition) if saved # self is the record + end + end + +For more information about the various behaviors added for DataMapper state +machines, see StateMachine::Integrations::DataMapper. + +=== Sequel + +Like the ActiveRecord integration, the Sequel integration adds support for +database transactions, automatically saving the record, named scopes, and +callbacks. For example, + + class Vehicle < Sequel::Model + state_machine :initial => 'parked' do + before_transition :to => 'idling', :do => :put_on_seatbelt + after_transition :to => 'parked' do |transition| + self.seatbelt = 'off' # self is the record + end + + event :ignite do + transition :to => 'idling', :from => 'parked' + end + end + + def put_on_seatbelt + ... + end + end + +For more information about the various behaviors added for Sequel state +machines, see StateMachine::Integrations::Sequel. + == Tools +=== Generating graphs + +This library comes with built-in support for generating di-graphs based on the +events, states, and transitions defined for a state machine using GraphViz[http://www.graphviz.org]. +This requires that both the <tt>ruby-graphviz</tt> gem and graphviz library be +installed on the system. + +==== Examples + +To generate a graph for a specific file / class: + + rake state_machine:draw FILE=vehicle.rb CLASS=Vehicle + +To save files to a specific path: + + rake state_machine:draw FILE=vehicle.rb CLASS=Vehicle TARGET=files + +To customize the image format: + + rake state_machine:draw FILE=vehicle.rb CLASS=Vehicle FORMAT=jpg + +To generate multiple state machine graphs: + + rake state_machine:draw FILE=vehicle.rb,car.rb CLASS=Vehicle,Car + +*Note* that this will generate a different file for every state machine defined +in the class. The generates files will an output filename of the format #{class_name}_#{attribute}.#{format}. + +For examples of actual images generated using this task, see those under the +test/examples folder. + +==== Ruby on Rails Integration + +There is a special integration Rake task for generating state machines for +classes used in a Ruby on Rails application. This task will load the application +environment, meaning that it's unnecessary to specify the actual file to load. + +For example, + + rake state_machine:draw:rails CLASS=Vehicle + +==== Merb Integration + +Like Ruby on Rails, there is a special integration Rake task for generating +state machines for classes used in a Merb application. This task will load the +application environment, meaning that it's unnecessary to specify the actual +files to load. + +For example, + + rake state_machine:draw:merb CLASS=Vehicle + +=== Interactive graphs + Jean Bovet - {Visual Automata Simulator}[http://www.cs.usfca.edu/~jbovet/vas.html]. This is a great tool for "simulating, visualizing and transforming finite state automata and Turing Machines". This tool can help in the creation of states and events for your models. It is cross-platform, written in Java. == Testing -Before you can run any tests, the following gem must be installed: -* plugin_test_helper[http://github.com/pluginaweek/plugin_test_helper] +To run the entire test suite (will test ActiveRecord, DataMapper, and Sequel +integrations if available): -To run against a specific version of Rails: + rake test - rake test RAILS_FRAMEWORK_ROOT=/path/to/rails - == Dependencies -* Rails 2.1 or later +By default, there are no dependencies. If using specific integrations, those +dependencies are listed below. + +* ActiveRecord[http://rubyonrails.org] integration: 2.1.0 or later +* DataMapper[http://datamapper.org] integration: 0.9.0 or later +* Sequel[http://sequel.rubyforge.org] integration: 2.8.0 or later == References * Scott Barron - acts_as_state_machine[http://elitists.textdriven.com/svn/plugins/acts_as_state_machine] * acts_as_enumeration[http://github.com/pluginaweek/acts_as_enumeration]