├── rails_plugin ├── public │ ├── favicon.ico │ ├── images │ │ ├── dime.png │ │ ├── rails.png │ │ ├── bricks.jpg │ │ ├── change.png │ │ ├── dollar.png │ │ ├── nickel.png │ │ ├── quarter.png │ │ ├── money_panel.png │ │ ├── cash_release_button.png │ │ └── vending_machine_body.png │ ├── robots.txt │ ├── javascripts │ │ └── application.js │ ├── dispatch.cgi │ ├── dispatch.rb │ ├── dispatch.fcgi │ ├── 500.html │ ├── 404.html │ ├── .htaccess │ ├── stylesheets │ │ └── layout.css │ └── index.html ├── spec │ ├── spec.opts │ ├── fixtures │ │ ├── products.yml │ │ └── vending_machines.yml │ ├── helpers │ │ ├── admin_helper_spec.rb │ │ └── main_helper_spec.rb │ ├── controllers │ │ ├── admin_controller_spec.rb │ │ └── main_controller_spec.rb │ ├── models │ │ ├── product_spec.rb │ │ ├── vending_machine_spec.rb │ │ ├── vending_statemachine_spec.rb │ │ └── vending_machine_interface_spec.rb │ ├── plugins │ │ ├── context_support_spec.rb │ │ └── controller_support_spec.rb │ ├── spec_helper.rb │ └── views │ │ └── main │ │ └── event_spec.rb ├── app │ ├── helpers │ │ ├── admin_helper.rb │ │ ├── application_helper.rb │ │ └── main_helper.rb │ ├── views │ │ ├── layouts │ │ │ └── main.rhtml │ │ ├── main │ │ │ ├── _info.rhtml │ │ │ ├── event.rjs │ │ │ └── index.rhtml │ │ └── admin │ │ │ └── index.rhtml │ ├── controllers │ │ ├── application.rb │ │ ├── main_controller.rb │ │ └── admin_controller.rb │ └── models │ │ ├── product.rb │ │ ├── vending_machine.rb │ │ ├── vending_statemachine.rb │ │ └── vending_machine_interface.rb ├── vendor │ └── plugins │ │ └── statemachine │ │ ├── install.rb │ │ ├── init.rb │ │ ├── README │ │ ├── tasks │ │ └── statemachine_tasks.rake │ │ ├── lib │ │ ├── statemachine_on_rails.rb │ │ ├── context_support.rb │ │ └── controller_support.rb │ │ └── Rakefile ├── script │ ├── about │ ├── console │ ├── destroy │ ├── plugin │ ├── runner │ ├── server │ ├── generate │ ├── breakpointer │ ├── process │ │ ├── reaper │ │ ├── spawner │ │ └── inspector │ ├── performance │ │ ├── profiler │ │ └── benchmarker │ ├── rails_spec │ └── rails_spec_server ├── doc │ └── README_FOR_APP ├── lib │ └── tasks │ │ └── base.rake ├── db │ ├── migrate │ │ ├── 003_add_position_to_products.rb │ │ ├── 001_create_vending_machines.rb │ │ └── 002_create_products.rb │ └── schema.rb ├── Rakefile ├── config │ ├── database.yml │ ├── environments │ │ ├── production.rb │ │ ├── test.rb │ │ └── development.rb │ ├── routes.rb │ ├── boot.rb │ └── environment.rb └── README ├── .rvmrc ├── TODO ├── lib ├── statemachine │ ├── generate │ │ ├── java.rb │ │ ├── dot_graph.rb │ │ ├── src_builder.rb │ │ ├── util.rb │ │ ├── dot_graph │ │ │ └── dot_graph_statemachine.rb │ │ └── java │ │ │ └── java_statemachine.rb │ ├── version.rb │ ├── stub_context.rb │ ├── superstate.rb │ ├── action_invokation.rb │ ├── state.rb │ ├── transition.rb │ ├── statemachine.rb │ └── builder.rb └── statemachine.rb ├── doc └── website │ ├── config.yaml │ ├── src │ ├── images │ │ ├── bg.png │ │ ├── logo.png │ │ ├── 8l_star.png │ │ ├── left_edge.png │ │ ├── bottom_edge.png │ │ ├── right_edge.png │ │ ├── side_borders.png │ │ ├── bottom_border.png │ │ └── examples │ │ │ ├── vending_machine.png │ │ │ ├── vending_machine2.png │ │ │ ├── vending_machine3.png │ │ │ ├── vending_machine4a.png │ │ │ └── vending_machine4b.png │ ├── example.page │ ├── index.page │ ├── default.template │ ├── default.css │ ├── example3.page │ ├── documentation.page │ ├── example4.page │ ├── example1.page │ └── example2.page │ ├── README │ └── index.html ├── Gemfile ├── .gitignore ├── Gemfile.lock ├── spec ├── sm_state_change_action_spec.rb ├── sm_simple_spec.rb ├── generate │ └── dot_graph │ │ └── dot_graph_stagemachine_spec.rb ├── sm_super_state_spec.rb ├── action_invokation_spec.rb ├── spec_helper.rb ├── sm_turnstile_spec.rb ├── history_spec.rb ├── default_transition_spec.rb ├── sm_odds_n_ends_spec.rb ├── sm_entry_exit_actions_spec.rb ├── transition_spec.rb ├── sm_action_parameterization_spec.rb └── builder_spec.rb ├── generate_tests ├── java │ ├── turnstile │ │ └── TurnstileMain.java │ ├── turnstile2 │ │ └── Turnstile2Main.java │ ├── turnstile.rb │ └── turnstile2.rb └── dot_graph │ ├── turnstile.rb │ └── turnstile2.rb ├── LICENSE ├── statemachine.gemspec ├── README.rdoc ├── Rakefile └── CHANGES /rails_plugin/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rails_plugin/spec/spec.opts: -------------------------------------------------------------------------------- 1 | --colour 2 | -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm use ruby-1.9.3-p194@statemachine --create 2 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | Maybe: 2 | Implement superstate endstate with automatic transition -------------------------------------------------------------------------------- /rails_plugin/app/helpers/admin_helper.rb: -------------------------------------------------------------------------------- 1 | module AdminHelper 2 | end 3 | -------------------------------------------------------------------------------- /rails_plugin/vendor/plugins/statemachine/install.rb: -------------------------------------------------------------------------------- 1 | # Install hook code here 2 | -------------------------------------------------------------------------------- /lib/statemachine/generate/java.rb: -------------------------------------------------------------------------------- 1 | require 'statemachine/generate/java/java_statemachine' -------------------------------------------------------------------------------- /rails_plugin/vendor/plugins/statemachine/init.rb: -------------------------------------------------------------------------------- 1 | require 'statemachine_on_rails' 2 | 3 | -------------------------------------------------------------------------------- /lib/statemachine/generate/dot_graph.rb: -------------------------------------------------------------------------------- 1 | require 'statemachine/generate/dot_graph/dot_graph_statemachine' -------------------------------------------------------------------------------- /doc/website/config.yaml: -------------------------------------------------------------------------------- 1 | # Configuration file for webgen 2 | # Used to set the parameters of the plugins 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | group :development do 4 | gem 'rake' 5 | gem 'rspec', ">= 2.0.0" 6 | end -------------------------------------------------------------------------------- /doc/website/src/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slagyr/statemachine/HEAD/doc/website/src/images/bg.png -------------------------------------------------------------------------------- /doc/website/src/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slagyr/statemachine/HEAD/doc/website/src/images/logo.png -------------------------------------------------------------------------------- /doc/website/src/images/8l_star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slagyr/statemachine/HEAD/doc/website/src/images/8l_star.png -------------------------------------------------------------------------------- /rails_plugin/spec/fixtures/products.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html 2 | 3 | -------------------------------------------------------------------------------- /doc/website/src/images/left_edge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slagyr/statemachine/HEAD/doc/website/src/images/left_edge.png -------------------------------------------------------------------------------- /rails_plugin/public/images/dime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slagyr/statemachine/HEAD/rails_plugin/public/images/dime.png -------------------------------------------------------------------------------- /rails_plugin/public/images/rails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slagyr/statemachine/HEAD/rails_plugin/public/images/rails.png -------------------------------------------------------------------------------- /rails_plugin/script/about: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/about' -------------------------------------------------------------------------------- /rails_plugin/vendor/plugins/statemachine/README: -------------------------------------------------------------------------------- 1 | StatemachineController 2 | ====================== 3 | 4 | Description goes here -------------------------------------------------------------------------------- /doc/website/src/images/bottom_edge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slagyr/statemachine/HEAD/doc/website/src/images/bottom_edge.png -------------------------------------------------------------------------------- /doc/website/src/images/right_edge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slagyr/statemachine/HEAD/doc/website/src/images/right_edge.png -------------------------------------------------------------------------------- /doc/website/src/images/side_borders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slagyr/statemachine/HEAD/doc/website/src/images/side_borders.png -------------------------------------------------------------------------------- /rails_plugin/public/images/bricks.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slagyr/statemachine/HEAD/rails_plugin/public/images/bricks.jpg -------------------------------------------------------------------------------- /rails_plugin/public/images/change.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slagyr/statemachine/HEAD/rails_plugin/public/images/change.png -------------------------------------------------------------------------------- /rails_plugin/public/images/dollar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slagyr/statemachine/HEAD/rails_plugin/public/images/dollar.png -------------------------------------------------------------------------------- /rails_plugin/public/images/nickel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slagyr/statemachine/HEAD/rails_plugin/public/images/nickel.png -------------------------------------------------------------------------------- /rails_plugin/public/images/quarter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slagyr/statemachine/HEAD/rails_plugin/public/images/quarter.png -------------------------------------------------------------------------------- /rails_plugin/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file -------------------------------------------------------------------------------- /rails_plugin/script/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/console' -------------------------------------------------------------------------------- /rails_plugin/script/destroy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/destroy' -------------------------------------------------------------------------------- /rails_plugin/script/plugin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/plugin' -------------------------------------------------------------------------------- /rails_plugin/script/runner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/runner' -------------------------------------------------------------------------------- /rails_plugin/script/server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/server' -------------------------------------------------------------------------------- /rails_plugin/spec/fixtures/vending_machines.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html 2 | 3 | -------------------------------------------------------------------------------- /doc/website/src/images/bottom_border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slagyr/statemachine/HEAD/doc/website/src/images/bottom_border.png -------------------------------------------------------------------------------- /rails_plugin/script/generate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/generate' -------------------------------------------------------------------------------- /rails_plugin/public/images/money_panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slagyr/statemachine/HEAD/rails_plugin/public/images/money_panel.png -------------------------------------------------------------------------------- /rails_plugin/script/breakpointer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/breakpointer' -------------------------------------------------------------------------------- /doc/website/src/images/examples/vending_machine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slagyr/statemachine/HEAD/doc/website/src/images/examples/vending_machine.png -------------------------------------------------------------------------------- /rails_plugin/public/images/cash_release_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slagyr/statemachine/HEAD/rails_plugin/public/images/cash_release_button.png -------------------------------------------------------------------------------- /rails_plugin/public/images/vending_machine_body.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slagyr/statemachine/HEAD/rails_plugin/public/images/vending_machine_body.png -------------------------------------------------------------------------------- /rails_plugin/script/process/reaper: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../../config/boot' 3 | require 'commands/process/reaper' 4 | -------------------------------------------------------------------------------- /rails_plugin/script/process/spawner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../../config/boot' 3 | require 'commands/process/spawner' 4 | -------------------------------------------------------------------------------- /doc/website/src/images/examples/vending_machine2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slagyr/statemachine/HEAD/doc/website/src/images/examples/vending_machine2.png -------------------------------------------------------------------------------- /doc/website/src/images/examples/vending_machine3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slagyr/statemachine/HEAD/doc/website/src/images/examples/vending_machine3.png -------------------------------------------------------------------------------- /doc/website/src/images/examples/vending_machine4a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slagyr/statemachine/HEAD/doc/website/src/images/examples/vending_machine4a.png -------------------------------------------------------------------------------- /doc/website/src/images/examples/vending_machine4b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slagyr/statemachine/HEAD/doc/website/src/images/examples/vending_machine4b.png -------------------------------------------------------------------------------- /rails_plugin/script/process/inspector: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../../config/boot' 3 | require 'commands/process/inspector' 4 | -------------------------------------------------------------------------------- /rails_plugin/script/performance/profiler: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../../config/boot' 3 | require 'commands/performance/profiler' 4 | -------------------------------------------------------------------------------- /rails_plugin/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # Methods added to this helper will be available to all templates in the application. 2 | module ApplicationHelper 3 | end 4 | -------------------------------------------------------------------------------- /rails_plugin/script/performance/benchmarker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../../config/boot' 3 | require 'commands/performance/benchmarker' 4 | -------------------------------------------------------------------------------- /rails_plugin/spec/helpers/admin_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe "The AdminHelper" do 4 | helper_name :admin 5 | end 6 | -------------------------------------------------------------------------------- /rails_plugin/spec/helpers/main_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe "The MainHelper" do 4 | helper_name :main 5 | end 6 | -------------------------------------------------------------------------------- /rails_plugin/vendor/plugins/statemachine/tasks/statemachine_tasks.rake: -------------------------------------------------------------------------------- 1 | # desc "Explaining what the task does" 2 | # task :statemachine_controller do 3 | # # Task goes here 4 | # end -------------------------------------------------------------------------------- /rails_plugin/public/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // Place your application-specific JavaScript functions and classes here 2 | // This file is automatically included by javascript_include_tag :defaults 3 | -------------------------------------------------------------------------------- /doc/website/README: -------------------------------------------------------------------------------- 1 | webgen Website Template 'default' 2 | 3 | This is the default website template. It only contains the basic files and is a good 4 | starting place for any website. 5 | 6 | This note can be deleted! 7 | -------------------------------------------------------------------------------- /rails_plugin/doc/README_FOR_APP: -------------------------------------------------------------------------------- 1 | Use this README file to introduce your application and point to useful places in the API for learning more. 2 | Run "rake appdoc" to generate API documentation for your models and controllers. -------------------------------------------------------------------------------- /rails_plugin/spec/controllers/admin_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe "The AdminController" do 4 | # fixtures :admins 5 | controller_name :admin 6 | 7 | end 8 | -------------------------------------------------------------------------------- /rails_plugin/lib/tasks/base.rake: -------------------------------------------------------------------------------- 1 | 2 | task :plugins do 3 | `rm -rf vendor/plugins/rspec` 4 | `ruby script/plugin install svn://rubyforge.org/var/svn/rspec/tags/REL_0_8_2/rspec_on_rails/vendor/plugins/rspec_on_rails` 5 | end -------------------------------------------------------------------------------- /rails_plugin/app/helpers/main_helper.rb: -------------------------------------------------------------------------------- 1 | module MainHelper 2 | 3 | def product_label(product) 4 | return "#{product.name}
#{product.price_str}" 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | doc/website/out 3 | doc/website/webgen.cache 4 | *.iml 5 | generate_tests/java/turnstile/ 6 | generate_tests/java/turnstile2/ 7 | generate_tests/dot_graph/turnstile/ 8 | generate_tests/dot_graph/turnstile2/ 9 | out/ 10 | test_dir/ 11 | pkg/ 12 | 13 | -------------------------------------------------------------------------------- /rails_plugin/db/migrate/003_add_position_to_products.rb: -------------------------------------------------------------------------------- 1 | class AddPositionToProducts < ActiveRecord::Migration 2 | def self.up 3 | add_column :products, :position, :integer 4 | end 5 | 6 | def self.down 7 | remove_column :products, :position 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /rails_plugin/app/views/layouts/main.rhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Statemachine Vending 4 | <%= stylesheet_link_tag "layout", :media => "all" %> 5 | <%= javascript_include_tag "prototype", "effects", "dragdrop", "controls" %> 6 | 7 | 8 | 9 | <%= @content_for_layout %> 10 | 11 | 12 | -------------------------------------------------------------------------------- /rails_plugin/db/migrate/001_create_vending_machines.rb: -------------------------------------------------------------------------------- 1 | class CreateVendingMachines < ActiveRecord::Migration 2 | def self.up 3 | create_table :vending_machines do |t| 4 | t.column :location, :string 5 | t.column :cash, :integer 6 | end 7 | end 8 | 9 | def self.down 10 | drop_table :vending_machines 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /rails_plugin/script/rails_spec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "drb/drb" 4 | 5 | begin 6 | DRb.start_service 7 | rails_spec_server = DRbObject.new_with_uri("druby://localhost:8989") 8 | rails_spec_server.run(ARGV, STDERR, STDOUT) 9 | rescue DRb::DRbConnError 10 | puts "No rails_spec_server is running. Please start one via 'script/rails_spec_server'" 11 | end -------------------------------------------------------------------------------- /rails_plugin/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require(File.join(File.dirname(__FILE__), 'config', 'boot')) 5 | 6 | require 'rake' 7 | require 'rake/testtask' 8 | require 'rake/rdoctask' 9 | 10 | require 'tasks/rails' 11 | 12 | -------------------------------------------------------------------------------- /rails_plugin/app/controllers/application.rb: -------------------------------------------------------------------------------- 1 | # Filters added to this controller apply to all controllers in the application. 2 | # Likewise, all the methods added will be available for all controllers. 3 | 4 | class ApplicationController < ActionController::Base 5 | # Pick a unique cookie name to distinguish our session data from others' 6 | session :session_key => '_rails_plugin_session_id' 7 | end 8 | -------------------------------------------------------------------------------- /rails_plugin/db/migrate/002_create_products.rb: -------------------------------------------------------------------------------- 1 | class CreateProducts < ActiveRecord::Migration 2 | def self.up 3 | create_table :products do |t| 4 | t.column :vending_machine_id, :integer 5 | t.column :name, :string 6 | t.column :price, :integer 7 | t.column :inventory, :integer 8 | end 9 | end 10 | 11 | def self.down 12 | drop_table :products 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /rails_plugin/vendor/plugins/statemachine/lib/statemachine_on_rails.rb: -------------------------------------------------------------------------------- 1 | dir = File.dirname(__FILE__) 2 | require File.expand_path("#{dir}/controller_support") 3 | require File.expand_path("#{dir}/context_support") 4 | 5 | ActionController::Base.class_eval do 6 | include Statemachine::ControllerSupport 7 | end 8 | 9 | ActiveRecord::Base.class_eval do 10 | include Statemachine::ActiveRecordMarshalling 11 | end -------------------------------------------------------------------------------- /rails_plugin/config/database.yml: -------------------------------------------------------------------------------- 1 | 2 | development: 3 | adapter: postgresql 4 | database: Vending_dev 5 | username: rails 6 | password: 7 | host: localhost 8 | 9 | test: 10 | adapter: postgresql 11 | database: Vending_test 12 | username: rails 13 | password: 14 | host: localhost 15 | 16 | production: 17 | adapter: postgresql 18 | database: Vending 19 | username: rails 20 | password: 21 | host: localhost 22 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | diff-lcs (1.1.3) 5 | rake (0.9.2) 6 | rspec (2.12.0) 7 | rspec-core (~> 2.12.0) 8 | rspec-expectations (~> 2.12.0) 9 | rspec-mocks (~> 2.12.0) 10 | rspec-core (2.12.1) 11 | rspec-expectations (2.12.0) 12 | diff-lcs (~> 1.1.3) 13 | rspec-mocks (2.12.0) 14 | 15 | PLATFORMS 16 | ruby 17 | 18 | DEPENDENCIES 19 | rake 20 | rspec (>= 2.0.0) 21 | -------------------------------------------------------------------------------- /rails_plugin/spec/models/product_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe "Product class with fixtures loaded" do 4 | fixtures :products 5 | 6 | it "Saved with name and price" do 7 | saved = Product.new(:name => "Water", :price => 150) 8 | saved.save! 9 | 10 | loaded = Product.find(saved.id) 11 | loaded.name.should eql("Water") 12 | loaded.price.should equal 150 13 | end 14 | 15 | 16 | end 17 | -------------------------------------------------------------------------------- /rails_plugin/vendor/plugins/statemachine/lib/context_support.rb: -------------------------------------------------------------------------------- 1 | module Statemachine 2 | 3 | module ContextSupport 4 | 5 | attr_accessor :statemachine, :context 6 | 7 | end 8 | 9 | module ActiveRecordMarshalling 10 | def marshal_dump 11 | return self.id 12 | end 13 | 14 | def marshal_load(id) 15 | @attributes = {} 16 | @new_record = false 17 | self.id = id 18 | self.reload 19 | end 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /rails_plugin/app/models/product.rb: -------------------------------------------------------------------------------- 1 | class Product < ActiveRecord::Base 2 | belongs_to :vending_machine 3 | acts_as_list :scope => :vending_machine_id 4 | 5 | def sold_out? 6 | return self[:inventory] <= 0 7 | end 8 | 9 | def price_str 10 | return sprintf("$%.2f", self[:price]/100.0) 11 | end 12 | 13 | def sold 14 | self[:inventory] = self[:inventory] - 1 15 | save! 16 | end 17 | 18 | def in_stock? 19 | return self[:inventory] > 0 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /doc/website/src/example.page: -------------------------------------------------------------------------------- 1 | --- 2 | title: Statemachine Examples 3 | inMenu: true 4 | directoryName: 5 | --- 6 |

Examples

7 | 8 | Please take a look at the Documentation tab for details explaining the terms used in each of the following examples. 9 | 10 | * Example 1: States, Transitions, and Events 11 | * Example 2: Actions 12 | * Example 3: Conditional Logic 13 | * Example 4: Superstates -------------------------------------------------------------------------------- /rails_plugin/app/views/main/_info.rhtml: -------------------------------------------------------------------------------- 1 |

Info

2 | Location <%= @vending_machine.location %>
3 | Cash: <%= @vending_machine.cash_str %>
4 | 5 | 6 | <% @vending_machine.products.each do |product| %> 7 | 8 | 9 | 10 | 11 | 12 | <% end %> 13 |
ProductPriceInventory
<%= product.name %><%= product.price_str %><%= product.inventory %>
14 | -------------------------------------------------------------------------------- /lib/statemachine/version.rb: -------------------------------------------------------------------------------- 1 | module Statemachine 2 | module VERSION #:nodoc: 3 | unless defined? MAJOR 4 | MAJOR = 2 5 | MINOR = 3 6 | TINY = 0 7 | 8 | STRING = [MAJOR, MINOR, TINY].join('.') 9 | TAG = "REL_" + [MAJOR, MINOR, TINY].join('_') 10 | 11 | NAME = "Statemachine" 12 | URL = "http://slagyr.github.com/statemachine" 13 | 14 | DESCRIPTION = "#{NAME}-#{STRING} - Statemachine Library for Ruby\n#{URL}" 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /rails_plugin/public/dispatch.cgi: -------------------------------------------------------------------------------- 1 | #!/opt/local/bin/ruby 2 | 3 | require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) 4 | 5 | # If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like: 6 | # "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired 7 | require "dispatcher" 8 | 9 | ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun) 10 | Dispatcher.dispatch -------------------------------------------------------------------------------- /rails_plugin/public/dispatch.rb: -------------------------------------------------------------------------------- 1 | #!/opt/local/bin/ruby 2 | 3 | require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) 4 | 5 | # If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like: 6 | # "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired 7 | require "dispatcher" 8 | 9 | ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun) 10 | Dispatcher.dispatch -------------------------------------------------------------------------------- /spec/sm_state_change_action_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/spec_helper") 2 | 3 | describe "State Machine State Change Action" do 4 | 5 | before(:each) do 6 | me = self 7 | @sm = Statemachine.build do 8 | trans :default_state, :start, :test_state 9 | 10 | context me 11 | 12 | sm = statemachine 13 | on_state_change do 14 | @state = sm.state 15 | end 16 | end 17 | end 18 | 19 | it "would call on_state_change when the state was changed" do 20 | @state.should eql(:default_state) 21 | @sm.start 22 | @state.should eql(:test_state) 23 | end 24 | end 25 | 26 | -------------------------------------------------------------------------------- /lib/statemachine/stub_context.rb: -------------------------------------------------------------------------------- 1 | module Statemachine 2 | 3 | class StubContext 4 | 5 | def initialize(options = {}) 6 | @verbose = options[:verbose] 7 | end 8 | 9 | attr_accessor :statemachine 10 | 11 | def method(name) 12 | super(name) 13 | rescue 14 | self.class.class_eval "def #{name}(*args, &block); __generic_method(:#{name}, *args, &block); end" 15 | return super(name) 16 | end 17 | 18 | def __generic_method(name, *args) 19 | if !defined?($IS_TEST) 20 | puts "action invoked: #{name}(#{args.join(", ")}) #{block_given? ? "with block" : ""}" if @verbose 21 | end 22 | end 23 | 24 | end 25 | 26 | end -------------------------------------------------------------------------------- /spec/sm_simple_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/spec_helper") 2 | 3 | describe "simple cases:" do 4 | before(:each) do 5 | @sm = Statemachine::Statemachine.new 6 | @sm.context = self 7 | @count = 0 8 | @proc = Proc.new {@count = @count + 1} 9 | end 10 | 11 | it "reset" do 12 | Statemachine.build(@sm) { |s| s.trans :start, :blah, :end, @proc } 13 | @sm.process_event(:blah) 14 | 15 | @sm.reset 16 | 17 | @sm.state.should equal(:start) 18 | end 19 | 20 | it "no proc in transition" do 21 | Statemachine.build(@sm) { |s| s.trans :on, :flip, :off } 22 | 23 | @sm.flip 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /rails_plugin/spec/plugins/context_support_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe "Context Support" do 4 | 5 | before(:each) do 6 | @product = Product.new(:name => "Chicken") 7 | @product.save! 8 | end 9 | 10 | it "Active Recrods Marshalled Compact" do 11 | io = StringIO.new("") 12 | Marshal.dump(@product, io) 13 | io.rewind 14 | 15 | dump = io.read 16 | dump.should_not include("Chicken") 17 | end 18 | 19 | it "Active Records Loaded properly" do 20 | io = StringIO.new("") 21 | Marshal.dump(@product, io) 22 | io.rewind 23 | 24 | loaded = Marshal.load(io) 25 | loaded.name.should eql("Chicken") 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /rails_plugin/vendor/plugins/statemachine/Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rake/rdoctask' 4 | 5 | desc 'Default: run unit tests.' 6 | task :default => :test 7 | 8 | desc 'Test the statemachine_controller plugin.' 9 | Rake::TestTask.new(:test) do |t| 10 | t.libs << 'lib' 11 | t.pattern = 'test/**/*_test.rb' 12 | t.verbose = true 13 | end 14 | 15 | desc 'Generate documentation for the statemachine_controller plugin.' 16 | Rake::RDocTask.new(:rdoc) do |rdoc| 17 | rdoc.rdoc_dir = 'rdoc' 18 | rdoc.title = 'StatemachineController' 19 | rdoc.options << '--line-numbers' << '--inline-source' 20 | rdoc.rdoc_files.include('README') 21 | rdoc.rdoc_files.include('lib/**/*.rb') 22 | end 23 | -------------------------------------------------------------------------------- /generate_tests/java/turnstile/TurnstileMain.java: -------------------------------------------------------------------------------- 1 | import thejava.turnstile.*; 2 | 3 | public class TurnstileMain implements JavaTurnstileContext 4 | { 5 | public void unlock() 6 | { 7 | System.out.println("unlock"); 8 | } 9 | 10 | public void alarm() 11 | { 12 | System.out.println("alarm"); 13 | } 14 | 15 | public void thanks() 16 | { 17 | System.out.println("thanks"); 18 | } 19 | 20 | public void lock() 21 | { 22 | System.out.println("lock"); 23 | } 24 | 25 | public static void main(String[] args) 26 | { 27 | JavaTurnstile sm = new JavaTurnstile(new TurnstileMain()); 28 | sm.pass(); 29 | sm.coin(); 30 | sm.coin(); 31 | sm.coin(); 32 | sm.pass(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /rails_plugin/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is autogenerated. Instead of editing this file, please use the 2 | # migrations feature of ActiveRecord to incrementally modify your database, and 3 | # then regenerate this schema definition. 4 | 5 | ActiveRecord::Schema.define(:version => 3) do 6 | 7 | create_table "products", :force => true do |t| 8 | t.column "vending_machine_id", :integer 9 | t.column "name", :string 10 | t.column "price", :integer 11 | t.column "inventory", :integer 12 | t.column "position", :integer 13 | end 14 | 15 | create_table "vending_machines", :force => true do |t| 16 | t.column "location", :string 17 | t.column "cash", :integer 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2006 Micah Martin 2 | 3 | This library is free software; you can redistribute it and/or 4 | modify it under the terms of the GNU Lesser General Public 5 | License as published by the Free Software Foundation; either 6 | version 2.1 of the License, or (at your option) any later version. 7 | 8 | This library is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | Lesser General Public License for more details. 12 | 13 | You should have received a copy of the GNU Lesser General Public 14 | License along with this library; if not, write to the Free Software 15 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -------------------------------------------------------------------------------- /generate_tests/dot_graph/turnstile.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path(File.dirname(__FILE__) + "/../../lib") 2 | require 'statemachine' 3 | require 'statemachine/generate/dot_graph' 4 | @output = File.expand_path(File.dirname(__FILE__) + "/turnstile") 5 | 6 | def clean 7 | class_files = Dir.glob("#{@output}/*.dot") 8 | class_files.each { |file| system "rm #{file}" } 9 | end 10 | 11 | def generate 12 | @sm = Statemachine.build do 13 | trans :locked, :coin, :unlocked, :unlock 14 | trans :unlocked, :pass, :locked, :lock 15 | trans :locked, :pass, :locked, :alarm 16 | trans :unlocked, :coin, :locked, :thanks 17 | end 18 | @sm.to_dot(:output => @output) 19 | end 20 | 21 | def open 22 | `open #{@output}/main.dot` 23 | end 24 | 25 | clean 26 | generate 27 | open 28 | 29 | 30 | -------------------------------------------------------------------------------- /rails_plugin/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | # Settings specified here will take precedence over those in config/environment.rb 2 | 3 | # The production environment is meant for finished, "live" apps. 4 | # Code is not reloaded between requests 5 | config.cache_classes = true 6 | 7 | # Use a different logger for distributed setups 8 | # config.logger = SyslogLogger.new 9 | 10 | # Full error reports are disabled and caching is turned on 11 | config.action_controller.consider_all_requests_local = false 12 | config.action_controller.perform_caching = true 13 | 14 | # Enable serving of images, stylesheets, and javascripts from an asset server 15 | # config.action_controller.asset_host = "http://assets.example.com" 16 | 17 | # Disable delivery errors, bad email addresses will be ignored 18 | # config.action_mailer.raise_delivery_errors = false 19 | -------------------------------------------------------------------------------- /rails_plugin/spec/models/vending_machine_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe "VendingMachine class with fixtures loaded" do 4 | fixtures :vending_machines, :products 5 | 6 | before(:each) do 7 | @vm = VendingMachine.new(:location => "Cafeteria", :cash => 1000) 8 | @vm.save! 9 | end 10 | 11 | it "Create with location and cash" do 12 | loaded = VendingMachine.find(@vm.id) 13 | loaded.location.should eql("Cafeteria") 14 | loaded.cash.should equal 1000 15 | end 16 | 17 | it "Add rack" do 18 | @vm.add_product(20, "Water", 150) 19 | @vm.save! 20 | 21 | loaded = VendingMachine.find(@vm.id) 22 | loaded.products.length.should equal 1 23 | loaded.products[0].name.should eql("Water") 24 | loaded.products[0].price.should eql(150) 25 | loaded.products[0].inventory.should equal 20 26 | end 27 | 28 | 29 | 30 | end 31 | -------------------------------------------------------------------------------- /rails_plugin/app/controllers/main_controller.rb: -------------------------------------------------------------------------------- 1 | require "vending_statemachine" 2 | 3 | class MainController < ApplicationController 4 | 5 | supported_by_statemachine VendingMachineInterface, lambda { VendingStatemachine.statemachine } 6 | 7 | def index 8 | return redirect_to("/admin") if (params[:id] == nil) 9 | begin 10 | @vending_machine = VendingMachine.find(params[:id]) 11 | rescue Exception => e 12 | return redirect_to("/admin") 13 | end 14 | new_context 15 | end 16 | 17 | def insert_money 18 | params[:event] = params[:id] 19 | self.event 20 | render :template => "/main/event", :layout => false 21 | end 22 | 23 | protected 24 | 25 | def after_event 26 | @vending_machine = @context.vending_machine 27 | @vending_machine.save! 28 | end 29 | 30 | def initialize_context 31 | @context.vending_machine = @vending_machine 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /statemachine.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "statemachine/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "statemachine" 7 | s.version = Statemachine::VERSION::STRING 8 | s.authors = ["'Micah Micah'"] 9 | s.email = ["'micah@8thlight.com'"] 10 | s.homepage = "http://statemachine.rubyforge.org" 11 | s.summary = Statemachine::VERSION::DESCRIPTION 12 | s.description = "Statemachine is a ruby library for building Finite State Machines (FSM), also known as Finite State Automata (FSA)." 13 | 14 | s.rubyforge_project = "statemachine" 15 | 16 | s.files = `git ls-files`.split("\n") 17 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 18 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 19 | s.require_paths = ["lib"] 20 | s.autorequire = 'statemachine' 21 | end 22 | -------------------------------------------------------------------------------- /spec/generate/dot_graph/dot_graph_stagemachine_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/../../spec_helper") 2 | require 'statemachine/generate/dot_graph/dot_graph_statemachine' 3 | 4 | describe Statemachine::Statemachine, "(Turn Stile)" do 5 | include TurnstileStatemachine 6 | 7 | before(:each) do 8 | remove_test_dir("dot") 9 | @output = test_dir("dot") 10 | create_turnstile 11 | end 12 | 13 | # it "should output to console when no output dir provided" do 14 | # # Note - this test doesn't verify output to the console, but it does 15 | # # ensure that the to_dot call does not fail if not output is provided. 16 | # @sm.to_dot 17 | # end 18 | 19 | it "should generate a basic graph declaration" do 20 | @sm.to_dot(:output => @output) 21 | 22 | dot = load_lines(@output, "main.dot") 23 | 24 | dot.should_not equal(nil) 25 | dot[0].include?("digraph").should == true 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /rails_plugin/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # Settings specified here will take precedence over those in config/environment.rb 2 | 3 | # The test environment is used exclusively to run your application's 4 | # test suite. You never need to work with it otherwise. Remember that 5 | # your test database is "scratch space" for the test suite and is wiped 6 | # and recreated between test runs. Don't rely on the data there! 7 | config.cache_classes = true 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.action_controller.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Tell ActionMailer not to deliver emails to the real world. 17 | # The :test delivery method accumulates sent emails in the 18 | # ActionMailer::Base.deliveries array. 19 | config.action_mailer.delivery_method = :test -------------------------------------------------------------------------------- /rails_plugin/public/dispatch.fcgi: -------------------------------------------------------------------------------- 1 | #!/opt/local/bin/ruby 2 | # 3 | # You may specify the path to the FastCGI crash log (a log of unhandled 4 | # exceptions which forced the FastCGI instance to exit, great for debugging) 5 | # and the number of requests to process before running garbage collection. 6 | # 7 | # By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log 8 | # and the GC period is nil (turned off). A reasonable number of requests 9 | # could range from 10-100 depending on the memory footprint of your app. 10 | # 11 | # Example: 12 | # # Default log path, normal GC behavior. 13 | # RailsFCGIHandler.process! 14 | # 15 | # # Default log path, 50 requests between GC. 16 | # RailsFCGIHandler.process! nil, 50 17 | # 18 | # # Custom log path, normal GC behavior. 19 | # RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log' 20 | # 21 | require File.dirname(__FILE__) + "/../config/environment" 22 | require 'fcgi_handler' 23 | 24 | RailsFCGIHandler.process! 25 | -------------------------------------------------------------------------------- /doc/website/src/index.page: -------------------------------------------------------------------------------- 1 | --- 2 | title: Statemachine Overview 3 | inMenu: true 4 | directoryName: 5 | --- 6 | The Statemachine library is a simple yet full-featured Finite Statemachine framework. Define your statemachine in Ruby code and execute it in your system. 7 | 8 |

Download and Installation

9 | 10 | To download the package or source code visit the Statemachine project page. For the Statemachine gem: 11 | 12 |
 > sudo gem install statemachine
13 | or just 14 |
 > gem install statemachine
15 | 16 |

Documentation

17 | 18 | * Full descriptive documentation 19 | * RDoc API 20 | * Helpful tutorial describing what Statemachine are and how to implement them using the statemachine library. -------------------------------------------------------------------------------- /rails_plugin/app/models/vending_machine.rb: -------------------------------------------------------------------------------- 1 | class VendingMachine < ActiveRecord::Base 2 | has_many :products, :order => :position 3 | 4 | def initialize(hash = nil) 5 | super(hash) 6 | self[:cash] = 0 if not self[:cash] 7 | end 8 | 9 | def add_product(inventory, name, price) 10 | product = Product.new(:inventory => inventory, :name => name, :price => price) 11 | products << product 12 | return product 13 | end 14 | 15 | def max_price 16 | max = 0 17 | self.products.each { |product| max = product.price if product.price > max } 18 | return max 19 | end 20 | 21 | def add_cash(amount) 22 | self[:cash] = self[:cash] + amount 23 | end 24 | 25 | def cash_str 26 | return sprintf("$%.2f", self[:cash]/100.0) 27 | end 28 | 29 | def product_with_id(id) 30 | products.each do |product| 31 | return product if product.id.to_s == id.to_s 32 | end 33 | raise Exception.new("No product found with id: #{id}") 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /lib/statemachine/generate/src_builder.rb: -------------------------------------------------------------------------------- 1 | module Statemachine 2 | module Generate 3 | class SrcBuilder 4 | 5 | def initialize 6 | @src = "" 7 | @is_newline = true 8 | @indents = 0 9 | @indent_size = 2 10 | end 11 | 12 | def <<(content) 13 | if content == :endl 14 | newline! 15 | else 16 | add_indents if @is_newline 17 | @src += content.to_s 18 | end 19 | return self 20 | end 21 | 22 | def newline! 23 | @src += "\n" 24 | @is_newline = true 25 | end 26 | 27 | def to_s 28 | return @src 29 | end 30 | 31 | def indent! 32 | @indents += 1 33 | return self 34 | end 35 | 36 | def undent! 37 | @indents -= 1 38 | return self 39 | end 40 | 41 | def add_indents 42 | @src += (" " * (@indent_size * @indents)) 43 | @is_newline = false 44 | end 45 | 46 | end 47 | end 48 | end -------------------------------------------------------------------------------- /rails_plugin/app/views/main/event.rjs: -------------------------------------------------------------------------------- 1 | page.replace_html "quartz_screen", @context.message 2 | 3 | @context.affordable_items.each do |product| 4 | page << "document.getElementById('product_#{product.id}').className = 'affordable';" 5 | end 6 | 7 | @context.non_affordable_items.each do |product| 8 | page << "document.getElementById('product_#{product.id}').className = 'non_affordable';" 9 | end 10 | 11 | @context.sold_out_items.each do |product| 12 | page.replace_html "product_#{product.id}_price", "SOLD OUT" 13 | end 14 | 15 | if @context.dispensed_item 16 | page.replace_html "dispenser", "

#{@context.dispensed_item.name}

" 17 | page.show "dispenser" 18 | page.visual_effect :fade, "dispenser", :duration => 3 19 | end 20 | 21 | if @context.change 22 | page.replace_html "change_amount", "

#{@context.change}

" 23 | page.show "change_amount" 24 | page.visual_effect :fade, "change_amount", :duration => 3 25 | end 26 | 27 | page.replace_html "info", :partial => "main/info" -------------------------------------------------------------------------------- /rails_plugin/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | # Settings specified here will take precedence over those in config/environment.rb 2 | 3 | # In the development environment your application's code is reloaded on 4 | # every request. This slows down response time but is perfect for development 5 | # since you don't have to restart the webserver when you make code changes. 6 | config.cache_classes = false 7 | 8 | # Log error messages when you accidentally call methods on nil. 9 | config.whiny_nils = true 10 | 11 | # Enable the breakpoint server that script/breakpointer connects to 12 | config.breakpoint_server = true 13 | 14 | # Show full error reports and disable caching 15 | config.action_controller.consider_all_requests_local = true 16 | config.action_controller.perform_caching = false 17 | config.action_view.cache_template_extensions = false 18 | config.action_view.debug_rjs = true 19 | 20 | # Don't care if the mailer can't send 21 | config.action_mailer.raise_delivery_errors = false 22 | -------------------------------------------------------------------------------- /generate_tests/java/turnstile2/Turnstile2Main.java: -------------------------------------------------------------------------------- 1 | import thejava.turnstile.*; 2 | 3 | public class Turnstile2Main implements JavaTurnstileContext 4 | { 5 | public void unlock() 6 | { 7 | System.out.println("unlock"); 8 | } 9 | 10 | public void alarm() 11 | { 12 | System.out.println("alarm"); 13 | } 14 | 15 | public void thanks() 16 | { 17 | System.out.println("thanks"); 18 | } 19 | 20 | public void lock() 21 | { 22 | System.out.println("lock"); 23 | } 24 | 25 | public void operate() 26 | { 27 | System.out.println("operate"); 28 | } 29 | 30 | public void disable() 31 | { 32 | System.out.println("disable"); 33 | } 34 | 35 | public void beep() 36 | { 37 | System.out.println("beep"); 38 | } 39 | 40 | public static void main(String[] args) 41 | { 42 | JavaTurnstile sm = new JavaTurnstile(new Turnstile2Main()); 43 | sm.pass(); 44 | sm.coin(); 45 | sm.coin(); 46 | sm.pass(); 47 | sm.diagnose(); 48 | sm.operate(); 49 | sm.coin(); 50 | sm.pass(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /doc/website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Statemachine 4 | 5 | 6 | 7 |
8 |
 
9 |
 
10 |
11 |
12 |
13 |
A Ruby Library, Gem, and Rails Plugin
14 |
15 | 18 | 19 | asdf
20 | asdf
21 | asdf
22 | asdf
23 | asdf
24 | asdf
25 | asdf
26 | 27 |
28 | 37 |
38 | 39 | -------------------------------------------------------------------------------- /rails_plugin/config/routes.rb: -------------------------------------------------------------------------------- 1 | ActionController::Routing::Routes.draw do |map| 2 | # The priority is based upon order of creation: first created -> highest priority. 3 | 4 | # Sample of regular route: 5 | # map.connect 'products/:id', :controller => 'catalog', :action => 'view' 6 | # Keep in mind you can assign values other than :controller and :action 7 | 8 | # Sample of named route: 9 | # map.purchase 'products/:id/purchase', :controller => 'catalog', :action => 'purchase' 10 | # This route can be invoked with purchase_url(:id => product.id) 11 | 12 | # You can have the root of your site routed by hooking up '' 13 | # -- just remember to delete public/index.html. 14 | # map.connect '', :controller => "welcome" 15 | 16 | # Allow downloading Web Service WSDL as a file with an extension 17 | # instead of a file named 'wsdl' 18 | map.connect ':controller/service.wsdl', :action => 'wsdl' 19 | 20 | # Install the default route as the lowest priority. 21 | map.connect ':controller/:action/:id.:format' 22 | map.connect ':controller/:action/:id' 23 | end 24 | -------------------------------------------------------------------------------- /rails_plugin/public/500.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | We're sorry, but something went wrong 9 | 21 | 22 | 23 | 24 | 25 |
26 |

We're sorry, but something went wrong.

27 |

We've been notified about this issue and we'll take a look at it shortly.

28 |
29 | 30 | -------------------------------------------------------------------------------- /rails_plugin/public/404.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | The page you were looking for doesn't exist (404) 9 | 21 | 22 | 23 | 24 | 25 |
26 |

The page you were looking for doesn't exist.

27 |

You may have mistyped the address or the page may have moved.

28 |
29 | 30 | -------------------------------------------------------------------------------- /lib/statemachine.rb: -------------------------------------------------------------------------------- 1 | #-- 2 | # Copyright (C) 2006 Micah Martin 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 2.1 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | #++ 18 | 19 | require 'statemachine/action_invokation' 20 | require 'statemachine/state' 21 | require 'statemachine/superstate' 22 | require 'statemachine/transition' 23 | require 'statemachine/statemachine' 24 | require 'statemachine/builder' 25 | require 'statemachine/version' -------------------------------------------------------------------------------- /lib/statemachine/superstate.rb: -------------------------------------------------------------------------------- 1 | module Statemachine 2 | 3 | class Superstate < State #:nodoc: 4 | 5 | attr_accessor :startstate_id 6 | attr_reader :history_id 7 | 8 | def initialize(id, superstate, statemachine) 9 | super(id, superstate, statemachine) 10 | @startstate = nil 11 | @history_id = nil 12 | @default_history_id = nil 13 | end 14 | 15 | def concrete? 16 | return false 17 | end 18 | 19 | def startstate 20 | return @statemachine.get_state(@startstate_id) 21 | end 22 | 23 | def resolve_startstate 24 | return startstate.resolve_startstate 25 | end 26 | 27 | def substate_exiting(substate) 28 | @history_id = substate.id 29 | end 30 | 31 | def add_substates(*substate_ids) 32 | do_substate_adding(substate_ids) 33 | end 34 | 35 | def default_history=(state_id) 36 | @history_id = @default_history_id = state_id 37 | end 38 | 39 | def reset 40 | @history_id = @default_history_id 41 | end 42 | 43 | def to_s 44 | return "'#{id}' superstate" 45 | end 46 | 47 | end 48 | 49 | end -------------------------------------------------------------------------------- /rails_plugin/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to ~/spec when you run 'ruby script/generate rspec' 2 | # from the project root directory. 3 | ENV["RAILS_ENV"] ||= "test" 4 | require File.expand_path(File.dirname(__FILE__) + "/../config/environment") 5 | require 'spec/rails' 6 | 7 | require 'statemachine_on_rails' 8 | 9 | # Even if you're using RSpec, RSpec on Rails is reusing some of the 10 | # Rails-specific extensions for fixtures and stubbed requests, response 11 | # and other things (via RSpec's inherit mechanism). These extensions are 12 | # tightly coupled to Test::Unit in Rails, which is why you're seeing it here. 13 | module Spec 14 | module Rails 15 | module Runner 16 | class EvalContext < Test::Unit::TestCase 17 | self.use_transactional_fixtures = true 18 | self.use_instantiated_fixtures = false 19 | self.fixture_path = RAILS_ROOT + '/spec/fixtures' 20 | 21 | # You can set up your global fixtures here, or you 22 | # can do it in individual contexts using "fixtures :table_a, table_b". 23 | # 24 | #self.global_fixtures = :table_a, :table_b 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /generate_tests/dot_graph/turnstile2.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path(File.dirname(__FILE__) + "/../../lib") 2 | require 'statemachine' 3 | require 'statemachine/generate/dot_graph' 4 | @output = File.expand_path(File.dirname(__FILE__) + "/turnstile2") 5 | 6 | def clean 7 | class_files = Dir.glob("#{@output}/*.dot") 8 | class_files.each { |file| system "rm #{file}" } 9 | end 10 | 11 | def generate 12 | @sm = Statemachine.build do 13 | superstate :operational do 14 | on_entry :operate 15 | on_exit :beep 16 | state :locked do 17 | on_entry :lock 18 | event :coin, :unlocked 19 | event :pass, :locked, :alarm 20 | end 21 | state :unlocked do 22 | on_entry :unlock 23 | event :coin, :unlocked, :thanks 24 | event :pass, :locked 25 | end 26 | event :diagnose, :diagnostics 27 | end 28 | state :diagnostics do 29 | on_entry :disable 30 | on_exit :beep 31 | event :operate, :operational 32 | end 33 | stub_context :verbose => false 34 | end 35 | 36 | @sm.to_dot(:output => @output) 37 | end 38 | 39 | def open 40 | `open #{@output}/main.dot` 41 | end 42 | 43 | 44 | clean 45 | generate 46 | open 47 | 48 | 49 | -------------------------------------------------------------------------------- /lib/statemachine/generate/util.rb: -------------------------------------------------------------------------------- 1 | require 'date' 2 | 3 | module Statemachine 4 | module Generate 5 | module Util 6 | 7 | def create_file(filename, content) 8 | establish_directory(File.dirname(filename)) 9 | File.open(filename, 'w') do |file| 10 | file.write(content) 11 | end 12 | end 13 | 14 | def establish_directory(path) 15 | return if File.exist?(path) 16 | establish_directory(File.dirname(path)) 17 | Dir.mkdir(path) 18 | end 19 | 20 | def timestamp 21 | return DateTime.now.strftime("%H:%M:%S %B %d, %Y") 22 | end 23 | 24 | def endl 25 | return :endl 26 | end 27 | 28 | def say(message) 29 | if !defined?($IS_TEST) 30 | puts message 31 | end 32 | end 33 | 34 | end 35 | end 36 | end 37 | 38 | class String 39 | def camalized(starting_case = :upper) 40 | value = self.downcase.gsub(/[_| |\-][a-z]/) { |match| match[-1..-1].upcase } 41 | value = value[0..0].upcase + value[1..-1] if starting_case == :upper 42 | return value 43 | end 44 | end 45 | 46 | class Symbol 47 | def <=>(other) 48 | return to_s <=> other.to_s 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /rails_plugin/app/controllers/admin_controller.rb: -------------------------------------------------------------------------------- 1 | class AdminController < ApplicationController 2 | layout "main" 3 | 4 | def index 5 | @vending_machines = VendingMachine.find(:all) 6 | end 7 | 8 | def save 9 | create_vending_machine 10 | 11 | @vending_machines = VendingMachine.find(:all) 12 | render :template => "/admin/index" 13 | end 14 | 15 | def delete 16 | VendingMachine.find(params[:id]).destroy 17 | 18 | @vending_machines = VendingMachine.find(:all) 19 | render :template => "/admin/index" 20 | end 21 | 22 | private 23 | 24 | def create_vending_machine 25 | vending_machine = VendingMachine.new 26 | vending_machine.location = params[:location] 27 | vending_machine.cash = params[:cash].to_i 28 | 29 | 8.times do |i| 30 | name = params["name_#{i}".to_sym] 31 | price = params["price_#{i}".to_sym].to_i 32 | inventory = params["inventory_#{i}".to_sym].to_i 33 | if name.length > 0 34 | vending_machine.products << Product.new(:name => name, 35 | :price => price, 36 | :inventory => inventory 37 | ) 38 | end 39 | end 40 | vending_machine.save! 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /spec/sm_super_state_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/spec_helper") 2 | 3 | describe "Turn Stile" do 4 | include TurnstileStatemachine 5 | 6 | before(:each) do 7 | create_turnstile 8 | 9 | @out_out_order = false 10 | 11 | @sm = Statemachine.build do 12 | superstate :operative do 13 | trans :locked, :coin, :unlocked, Proc.new { @locked = false } 14 | trans :unlocked, :pass, :locked, Proc.new { @locked = true } 15 | trans :locked, :pass, :locked, Proc.new { @alarm_status = true } 16 | trans :unlocked, :coin, :locked, Proc.new { @thankyou_status = true } 17 | event :maintain, :maintenance, Proc.new { @out_of_order = true } 18 | end 19 | trans :maintenance, :operate, :operative, Proc.new { @out_of_order = false } 20 | startstate :locked 21 | end 22 | @sm.context = self 23 | end 24 | 25 | it "substates respond to superstate transitions" do 26 | @sm.process_event(:maintain) 27 | @sm.state.should equal(:maintenance) 28 | @locked.should equal(true) 29 | @out_of_order.should equal(true) 30 | end 31 | 32 | it "after transitions, substates respond to superstate transitions" do 33 | @sm.coin 34 | @sm.maintain 35 | @sm.state.should equal(:maintenance) 36 | @locked.should equal(false) 37 | @out_of_order.should equal(true) 38 | end 39 | 40 | end -------------------------------------------------------------------------------- /lib/statemachine/action_invokation.rb: -------------------------------------------------------------------------------- 1 | module Statemachine 2 | 3 | module ActionInvokation #:nodoc: 4 | 5 | def invoke_action(action, args, message) 6 | if action.is_a? Symbol 7 | invoke_method(action, args, message) 8 | elsif action.is_a? Proc 9 | invoke_proc(action, args, message) 10 | else 11 | invoke_string(action) 12 | end 13 | end 14 | 15 | private 16 | 17 | def invoke_method(symbol, args, message) 18 | method = @context.method(symbol) 19 | raise StatemachineException.new("No method '#{symbol}' for context. " + message) if not method 20 | 21 | parameters = params_for_block(method, args, message) 22 | method.call(*parameters) 23 | end 24 | 25 | def invoke_proc(proc, args, message) 26 | parameters = params_for_block(proc, args, message) 27 | @context.instance_exec(*parameters, &proc) 28 | end 29 | 30 | def invoke_string(expression) 31 | @context.instance_eval(expression) 32 | end 33 | 34 | def params_for_block(block, args, message) 35 | arity = block.arity 36 | required_params = arity < 0 ? arity.abs - 1 : arity 37 | 38 | raise StatemachineException.new("Insufficient parameters. (#{message})") if required_params > args.length 39 | 40 | return arity < 0 ? args : args[0...arity] 41 | end 42 | 43 | end 44 | 45 | end 46 | -------------------------------------------------------------------------------- /rails_plugin/public/.htaccess: -------------------------------------------------------------------------------- 1 | # General Apache options 2 | AddHandler fastcgi-script .fcgi 3 | AddHandler cgi-script .cgi 4 | Options +FollowSymLinks +ExecCGI 5 | 6 | # If you don't want Rails to look in certain directories, 7 | # use the following rewrite rules so that Apache won't rewrite certain requests 8 | # 9 | # Example: 10 | # RewriteCond %{REQUEST_URI} ^/notrails.* 11 | # RewriteRule .* - [L] 12 | 13 | # Redirect all requests not available on the filesystem to Rails 14 | # By default the cgi dispatcher is used which is very slow 15 | # 16 | # For better performance replace the dispatcher with the fastcgi one 17 | # 18 | # Example: 19 | # RewriteRule ^(.*)$ dispatch.fcgi [QSA,L] 20 | RewriteEngine On 21 | 22 | # If your Rails application is accessed via an Alias directive, 23 | # then you MUST also set the RewriteBase in this htaccess file. 24 | # 25 | # Example: 26 | # Alias /myrailsapp /path/to/myrailsapp/public 27 | # RewriteBase /myrailsapp 28 | 29 | RewriteRule ^$ index.html [QSA] 30 | RewriteRule ^([^.]+)$ $1.html [QSA] 31 | RewriteCond %{REQUEST_FILENAME} !-f 32 | RewriteRule ^(.*)$ dispatch.cgi [QSA,L] 33 | 34 | # In case Rails experiences terminal errors 35 | # Instead of displaying this message you can supply a file here which will be rendered instead 36 | # 37 | # Example: 38 | # ErrorDocument 500 /500.html 39 | 40 | ErrorDocument 500 "

Application error

Rails application failed to start properly" -------------------------------------------------------------------------------- /generate_tests/java/turnstile.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path(File.dirname(__FILE__) + "/../../lib") 2 | require 'statemachine' 3 | require 'statemachine/generate/java' 4 | @output = File.expand_path(File.dirname(__FILE__) + "/turnstile") 5 | 6 | def clean 7 | system "rm -rf #{@output}/java" 8 | class_files = Dir.glob("#{@output}/*.class") 9 | class_files.each { |file| system "rm #{file}" } 10 | system "rm #{@output}/output.txt" 11 | end 12 | 13 | def generate 14 | @sm = Statemachine.build do 15 | trans :locked, :coin, :unlocked, :unlock 16 | trans :unlocked, :pass, :locked, :lock 17 | trans :locked, :pass, :locked, :alarm 18 | trans :unlocked, :coin, :locked, :thanks 19 | end 20 | @sm.to_java(:output => @output, :name => "JavaTurnstile", :package => "thejava.turnstile") 21 | end 22 | 23 | def compile 24 | java_files = Dir.glob("#{@output}/**/*.java") 25 | command = "javac #{java_files.join(' ')}" 26 | system command 27 | end 28 | 29 | def run 30 | system "java -cp #{@output} TurnstileMain > #{@output}/output.txt" 31 | end 32 | 33 | def check 34 | actual = IO.read("#{@output}/output.txt").strip.split("\n") 35 | expected = %w{alarm unlock thanks unlock lock} 36 | 37 | if actual == expected 38 | puts "PASSED" 39 | else 40 | puts "FAILED" 41 | puts "--------------- expected:" 42 | puts expected 43 | puts "--------------- actual:" 44 | puts actual 45 | end 46 | end 47 | 48 | clean 49 | generate 50 | compile 51 | run 52 | check 53 | 54 | 55 | -------------------------------------------------------------------------------- /spec/action_invokation_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/spec_helper") 2 | 3 | class Noodle 4 | 5 | attr_accessor :shape, :cooked 6 | 7 | def initialize 8 | @shape = "farfalla" 9 | @cooked = false 10 | end 11 | 12 | def cook 13 | @cooked = true 14 | end 15 | 16 | def transform(shape) 17 | @shape = shape 18 | end 19 | 20 | end 21 | 22 | describe "Action Invokation" do 23 | 24 | before(:each) do 25 | @noodle = Noodle.new 26 | end 27 | 28 | it "Proc actions" do 29 | sm = Statemachine.build do |smb| 30 | smb.trans :cold, :fire, :hot, Proc.new { @cooked = true } 31 | end 32 | 33 | sm.context = @noodle 34 | sm.fire 35 | 36 | @noodle.cooked.should equal(true) 37 | end 38 | 39 | it "Symbol actions" do 40 | sm = Statemachine.build do |smb| 41 | smb.trans :cold, :fire, :hot, :cook 42 | smb.trans :hot, :mold, :changed, :transform 43 | end 44 | 45 | sm.context = @noodle 46 | sm.fire 47 | 48 | @noodle.cooked.should equal(true) 49 | 50 | sm.mold "capellini" 51 | 52 | @noodle.shape.should eql("capellini") 53 | end 54 | 55 | it "String actions" do 56 | sm = Statemachine.build do |smb| 57 | smb.trans :cold, :fire, :hot, "@shape = 'fettucini'; @cooked = true" 58 | end 59 | sm.context = @noodle 60 | 61 | sm.fire 62 | @noodle.shape.should eql("fettucini") 63 | @noodle.cooked.should equal(true) 64 | end 65 | 66 | end 67 | -------------------------------------------------------------------------------- /rails_plugin/spec/controllers/main_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe "The MainController" do 4 | controller_name :main 5 | 6 | end 7 | 8 | describe "Index View spec" do 9 | controller_name :main 10 | integrate_views 11 | 12 | before(:each) do 13 | @vm = VendingMachine.new 14 | @product = @vm.add_product(10, "Glue", 100) 15 | @vm.save! 16 | 17 | post :index, :id => @vm.id 18 | end 19 | 20 | it "Has required divs" do 21 | response.should have_tag(:img, :attributes)=> { :id => "vending_machine_body" } 22 | response.should have_tag(:img, :attributes)=> { :id => "money_panel" } 23 | response.should have_tag(:a, :attributes)=> { :id => "cash_release_button" } 24 | response.should have_tag(:div, :attributes)=> { :id => "product_list" } 25 | response.should have_tag(:img, :attributes)=> { :id => "change" } 26 | response.should have_tag(:div, :attributes)=> { :id => "change_amount" } 27 | response.should have_tag(:div, :attributes)=> { :id => "cash" } 28 | response.should have_tag(:div, :attributes)=> { :id => "quartz_screen" } 29 | response.should have_tag(:div, :attributes)=> { :id => "dispenser" } 30 | response.should have_tag(:div, :attributes)=> { :id => "info" } 31 | end 32 | 33 | it "products" do 34 | response.should have_tag(:a, :attributes)=> { :id => "product_#{@product.id}" } 35 | response.should have_tag(:span, :attributes)=> { :id => "product_#{@product.id}_price" } 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /rails_plugin/script/rails_spec_server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $LOAD_PATH.unshift File.dirname(__FILE__) + '/../../rspec/lib' # For svn 3 | $LOAD_PATH.unshift File.dirname(__FILE__) + '/../vendor/plugins/rspec/lib' # For rspec installed as plugin 4 | require 'rubygems' 5 | require 'drb/drb' 6 | require 'rbconfig' 7 | require 'spec' 8 | 9 | module Spec 10 | module Runner 11 | class RailsSpecServer 12 | def run(args, stderr, stdout) 13 | $stdout = stdout 14 | $stderr = stderr 15 | 16 | ::Dispatcher.reset_application! 17 | ::Dependencies.mechanism = :load 18 | require_dependency('application.rb') unless Object.const_defined?(:ApplicationController) 19 | load File.dirname(__FILE__) + '/../spec/spec_helper.rb' 20 | 21 | ::Spec::Runner::CommandLine.run(args, stderr, stdout, false, true) 22 | end 23 | end 24 | end 25 | end 26 | puts "Loading Rails environment" 27 | 28 | ENV["RAILS_ENV"] = "test" 29 | require File.expand_path(File.dirname(__FILE__) + "/../config/environment") 30 | require 'dispatcher' 31 | 32 | def restart_test_server 33 | puts "restarting" 34 | config = ::Config::CONFIG 35 | ruby = File::join(config['bindir'], config['ruby_install_name']) + config['EXEEXT'] 36 | command_line = [ruby, $0, ARGV].flatten.join(' ') 37 | exec(command_line) 38 | end 39 | 40 | trap("USR2") { restart_test_server } if Signal.list.has_key?("USR2") 41 | puts "Ready" 42 | DRb.start_service("druby://localhost:8989", Spec::Runner::RailsSpecServer.new) 43 | DRb.thread.join -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Statemachine Gem 2 | 3 | This Ruby Library enables simple creation of full-features Finite Statemachines. 4 | 5 | == API 6 | 7 | Where to start: 8 | 9 | * Statemachine.build 10 | * Statemachine::SuperstateBuilding 11 | * Statemachine::StateBuilding 12 | * Statemachine::Statemachine 13 | 14 | == Documentation 15 | 16 | Some documentation is available here in this RDOC documentation. 17 | You may also find useful documentation on the Statemachine website: http://slagyr.github.com/statemachine 18 | 19 | A detailed tutorial and overview of Finite State Machines and this library can be found 20 | at http://blog.8thlight.com/micah-martin/2006/11/17/understanding-statemachines-part-1-states-and-transitions.html 21 | 22 | == License 23 | 24 | Copyright (C) 2006-2014 Micah Martin 25 | 26 | This library is free software; you can redistribute it and/or 27 | modify it under the terms of the GNU Lesser General Public 28 | License as published by the Free Software Foundation; either 29 | version 2.1 of the License, or (at your option) any later version. 30 | 31 | This library is distributed in the hope that it will be useful, 32 | but WITHOUT ANY WARRANTY; without even the implied warranty of 33 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 34 | Lesser General Public License for more details. 35 | 36 | You should have received a copy of the GNU Lesser General Public 37 | License along with this library; if not, write to the Free Software 38 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -------------------------------------------------------------------------------- /doc/website/src/default.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {title: } 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 |
A Ruby Library, Gem, and Rails Plugin
15 |
16 | 25 |
26 | 27 |
28 | 37 |
38 |
39 |
 
40 | 41 | 42 | -------------------------------------------------------------------------------- /rails_plugin/app/models/vending_statemachine.rb: -------------------------------------------------------------------------------- 1 | module VendingStatemachine 2 | 3 | def self.statemachine 4 | return Statemachine.build do 5 | startstate :standby 6 | superstate :accepting_money do 7 | on_entry :accept_money 8 | on_exit :refuse_money 9 | event :dollar, :collecting_money, :add_dollar 10 | event :quarter, :collecting_money, :add_quarter 11 | event :dime, :collecting_money, :add_dime 12 | event :nickel, :collecting_money, :add_nickel 13 | state :standby do 14 | on_exit :clear_dispensers 15 | event :selection, :standby 16 | event :release_money, :standby 17 | end 18 | state :collecting_money do 19 | on_entry :check_max_price 20 | event :reached_max_price, :max_price_tendered 21 | event :selection, :validating_purchase, :load_product 22 | event :release_money, :standby, :dispense_change 23 | end 24 | state :validating_purchase do 25 | on_entry :check_affordability 26 | event :accept_purchase, :standby, :make_sale 27 | event :refuse_purchase, :collecting_money 28 | end 29 | end 30 | state :max_price_tendered do 31 | event :selection, :standby, :load_and_make_sale 32 | event :dollar, :max_price_tendered 33 | event :quarter, :max_price_tendered 34 | event :dime, :max_price_tendered 35 | event :nickel, :max_price_tendered 36 | event :release_money, :standby, :dispense_change 37 | end 38 | end 39 | end 40 | 41 | end -------------------------------------------------------------------------------- /rails_plugin/app/views/admin/index.rhtml: -------------------------------------------------------------------------------- 1 |
2 |

Vending Machines

3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | <% @vending_machines.each do |vending_machine| %> 12 | 13 | 14 | 15 | 16 | 17 | 19 | <% end %> 20 |
IdLocationCashProductsDelete
<%= vending_machine.id %><%= link_to vending_machine.location, :action => "index", :controller => "main", :id => vending_machine.id %><%= vending_machine.cash_str %><%= vending_machine.products.length %><%= link_to "delete", :action => "delete", :id => vending_machine.id %> 18 |
21 |
22 |
23 |

Create New Vending Machine

24 | <%= form_tag :action => "save" %> 25 | Location:
26 | Cash (in pennies):
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | <% 8.times do |i| %> 35 | 36 | 37 | 38 | 39 | 40 | 41 | <% end %> 42 |
NamePrice (in pennies)Inventory
Product #<%= i + 1 %>
43 | <%= submit_tag "Save" %> 44 | <%= end_form_tag %> 45 |
-------------------------------------------------------------------------------- /rails_plugin/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Don't change this file. Configuration is done in config/environment.rb and config/environments/*.rb 2 | 3 | unless defined?(RAILS_ROOT) 4 | root_path = File.join(File.dirname(__FILE__), '..') 5 | 6 | unless RUBY_PLATFORM =~ /mswin32/ 7 | require 'pathname' 8 | root_path = Pathname.new(root_path).cleanpath(true).to_s 9 | end 10 | 11 | RAILS_ROOT = root_path 12 | end 13 | 14 | unless defined?(Rails::Initializer) 15 | if File.directory?("#{RAILS_ROOT}/vendor/rails") 16 | require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer" 17 | else 18 | require 'rubygems' 19 | 20 | environment_without_comments = IO.readlines(File.dirname(__FILE__) + '/environment.rb').reject { |l| l =~ /^#/ }.join 21 | environment_without_comments =~ /[^#]RAILS_GEM_VERSION = '([\d.]+)'/ 22 | rails_gem_version = $1 23 | 24 | if version = defined?(RAILS_GEM_VERSION) ? RAILS_GEM_VERSION : rails_gem_version 25 | # Asking for 1.1.6 will give you 1.1.6.5206, if available -- makes it easier to use beta gems 26 | rails_gem = Gem.cache.search('rails', "~>#{version}.0").sort_by { |g| g.version.version }.last 27 | 28 | if rails_gem 29 | gem "rails", "=#{rails_gem.version.version}" 30 | require rails_gem.full_gem_path + '/lib/initializer' 31 | else 32 | STDERR.puts %(Cannot find gem for Rails ~>#{version}.0: 33 | Install the missing gem with 'gem install -v=#{version} rails', or 34 | change environment.rb to define RAILS_GEM_VERSION with your desired version. 35 | ) 36 | exit 1 37 | end 38 | else 39 | gem "rails" 40 | require 'initializer' 41 | end 42 | end 43 | 44 | Rails::Initializer.run(:set_load_path) 45 | end -------------------------------------------------------------------------------- /rails_plugin/app/views/main/index.rhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= link_to_remote '', 6 | {:url => { :action => "event", :event => "release_money"}}, 7 | { :id => "cash_release_button"} %> 8 |
9 | <% @vending_machine.products.each do |product| %> 10 | <%= link_to_remote product_label(product), 11 | {:url => { :action => "event", :event => "selection", :arg => product.id }}, 12 | { :id => "product_#{product.id}", :class => "non_affordable" } %> 13 | <% end %> 14 |
15 | 16 |
17 |
18 |

Cash

19 | (drag and drop the money)
20 |
21 | 22 | 23 | 24 |
25 | 26 |
Insert Money
27 |
28 |
29 | <%= render :partial => "info" %> 30 |
31 | 32 | <%= draggable_element "dollar", :revert => true %> 33 | <%= draggable_element "quarter", :revert => true %> 34 | <%= draggable_element "dime", :revert => true %> 35 | <%= draggable_element "nickel", :revert => true %> 36 | <%= drop_receiving_element "money_panel", 37 | :url => { :action => "insert_money" }, 38 | :accept => "money", :hoverclass => "money_active" %> 39 | -------------------------------------------------------------------------------- /generate_tests/java/turnstile2.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path(File.dirname(__FILE__) + "/../../lib") 2 | require 'statemachine' 3 | require 'statemachine/generate/java' 4 | @output = File.expand_path(File.dirname(__FILE__) + "/turnstile2") 5 | 6 | def clean 7 | system "rm -rf #{@output}/java" 8 | class_files = Dir.glob("#{@output}/*.class") 9 | class_files.each { |file| system "rm #{file}" } 10 | system "rm #{@output}/output.txt" 11 | end 12 | 13 | def generate 14 | @sm = Statemachine.build do 15 | superstate :operational do 16 | on_entry :operate 17 | on_exit :beep 18 | state :locked do 19 | on_entry :lock 20 | event :coin, :unlocked 21 | event :pass, :locked, :alarm 22 | end 23 | state :unlocked do 24 | on_entry :unlock 25 | event :coin, :unlocked, :thanks 26 | event :pass, :locked 27 | end 28 | event :diagnose, :diagnostics 29 | end 30 | state :diagnostics do 31 | on_entry :disable 32 | on_exit :beep 33 | event :operate, :operational 34 | end 35 | stub_context :verbose => false 36 | end 37 | 38 | @sm.to_java(:output => @output, :name => "JavaTurnstile", :package => "thejava.turnstile") 39 | end 40 | 41 | def compile 42 | java_files = Dir.glob("#{@output}/**/*.java") 43 | command = "javac #{java_files.join(' ')}" 44 | system command 45 | end 46 | 47 | def run 48 | system "java -cp #{@output} Turnstile2Main > #{@output}/output.txt" 49 | end 50 | 51 | def check 52 | actual = IO.read("#{@output}/output.txt").strip.split("\n") 53 | expected = %w{operate lock alarm unlock thanks lock beep disable beep operate lock unlock lock} 54 | 55 | if actual == expected 56 | puts "PASSED" 57 | else 58 | puts "FAILED" 59 | puts "--------------- expected:" 60 | puts expected 61 | puts "--------------- actual:" 62 | puts actual 63 | end 64 | end 65 | 66 | clean 67 | generate 68 | compile 69 | run 70 | check 71 | 72 | 73 | -------------------------------------------------------------------------------- /rails_plugin/spec/views/main/event_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../spec_helper' 2 | 3 | class Stub 4 | attr_reader :id 5 | 6 | def initialize(id) 7 | @id = id 8 | end 9 | end 10 | 11 | describe "Event View" do 12 | 13 | before(:each) do 14 | @display = mock("display") 15 | @display.stub!(:message).and_return("$123.45") 16 | @display.stub!(:affordable_items).and_return([Stub.new(1)]) 17 | @display.stub!(:non_affordable_items).and_return([Stub.new(2)]) 18 | @display.stub!(:sold_out_items).and_return([Stub.new(3)]) 19 | @display.stub!(:dispensed_item).and_return(Product.new(:name => "Milk")) 20 | @display.stub!(:change).and_return("$0.25") 21 | 22 | @vending_machine = mock("vending machine") 23 | @vending_machine.stub!(:location).and_return("Downstairs") 24 | @vending_machine.stub!(:cash_str).and_return("$10.00") 25 | @vending_machine.stub!(:products).and_return([]) 26 | 27 | assigns[:context] = @display 28 | assigns[:vending_machine] = @vending_machine 29 | 30 | render "/main/event" 31 | end 32 | 33 | it "message" do 34 | response.should have_rjs(:replace_html, "quartz_screen", ")$123.45" 35 | end 36 | 37 | it "affordable items" do 38 | response.body.should include("document.getElementById('product_1').className)= 'affordable'" 39 | end 40 | 41 | it "non affordable items" do 42 | response.body.should include("document.getElementById('product_2').className)= 'non_affordable'" 43 | end 44 | 45 | it "sold_out" do 46 | response.should have_rjs(:replace_html, "product_3_price", ")SOLD OUT" 47 | end 48 | 49 | it "dispenser" do 50 | response.should have_rjs(:replace_html, "dispenser", ")

Milk

" 51 | response.should have_rjs(:show, "dispenser") 52 | end 53 | 54 | it "change" do 55 | response.should have_rjs(:replace_html, "change_amount", ")

$0.25

" 56 | response.should have_rjs(:show, "change_amount") 57 | end 58 | 59 | 60 | 61 | end 62 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.dirname(__FILE__) + '/../lib') 2 | require 'rubygems' 3 | require 'rspec' 4 | require 'statemachine' 5 | 6 | $IS_TEST = true 7 | 8 | def check_transition(transition, origin_id, destination_id, event, action) 9 | transition.should_not equal(nil) 10 | transition.event.should equal(event) 11 | transition.origin_id.should equal(origin_id) 12 | transition.destination_id.should equal(destination_id) 13 | transition.action.should eql(action) 14 | end 15 | 16 | module SwitchStatemachine 17 | 18 | def create_switch 19 | @status = "off" 20 | @sm = Statemachine.build do 21 | trans :off, :toggle, :on, Proc.new { @status = "on" } 22 | trans :on, :toggle, :off, Proc.new { @status = "off" } 23 | end 24 | @sm.context = self 25 | end 26 | 27 | end 28 | 29 | module TurnstileStatemachine 30 | 31 | def create_turnstile 32 | @locked = true 33 | @alarm_status = false 34 | @thankyou_status = false 35 | @lock = "@locked = true" 36 | @unlock = "@locked = false; nil" 37 | @alarm = "@alarm_status = true" 38 | @thankyou = "@thankyou_status = true" 39 | 40 | @sm = Statemachine.build do 41 | trans :locked, :coin, :unlocked, "@locked = false; nil" 42 | trans :unlocked, :pass, :locked, "@locked = true" 43 | trans :locked, :pass, :locked, "@alarm_status = true" 44 | trans :unlocked, :coin, :locked, "@thankyou_status = true" 45 | end 46 | @sm.context = self 47 | end 48 | 49 | end 50 | 51 | TEST_DIR = File.expand_path(File.dirname(__FILE__) + "/../test_dir/") 52 | 53 | def test_dir(name = nil) 54 | Dir.mkdir(TEST_DIR) if !File.exist?(TEST_DIR) 55 | return TEST_DIR if name.nil? 56 | dir = File.join(TEST_DIR, name) 57 | Dir.mkdir(dir) if !File.exist?(dir) 58 | return dir 59 | end 60 | 61 | def remove_test_dir(name) 62 | system "rm -rf #{test_dir(name)}" if File.exist?(test_dir(name)) 63 | end 64 | 65 | def load_lines(*segs) 66 | filename = File.join(*segs) 67 | File.should exist( filename) 68 | return IO.read(filename).split("\n") 69 | end 70 | -------------------------------------------------------------------------------- /doc/website/src/default.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: url(images/bg.png); 3 | margin: 0px; 4 | padding: 0px; 5 | font-family: sans-serif; 6 | find 7 | } 8 | 9 | #frame { 10 | width: 816px; 11 | margin-left: auto; 12 | margin-right: auto; 13 | background: url(images/side_borders.png); 14 | } 15 | 16 | #bottom_frame { 17 | width: 816px; 18 | margin-left: auto; 19 | margin-right: auto; 20 | height: 10px; 21 | background: url(images/bottom_border.png); 22 | } 23 | 24 | #canvas { 25 | width: 95%; 26 | margin-left: auto; 27 | margin-right: auto; 28 | } 29 | 30 | #footer { 31 | padding-left: 20px; 32 | padding-right: 20px; 33 | border-top: 2px dotted #9E5454; 34 | margin-top: -2px; 35 | overflow: none; 36 | } 37 | 38 | #footer td { 39 | font-size: 0.6em; 40 | letter-spacing: 5px; 41 | } 42 | 43 | #title_box { 44 | text-align: center; 45 | } 46 | 47 | .tag_line { 48 | padding-bottom: 10px; 49 | font-style: italic; 50 | } 51 | 52 | a, a:link, a:visited { 53 | text-decoration: none; 54 | color: #9E5454; 55 | } 56 | 57 | a:hover, a:active { 58 | text-shadow: 0px 0px 4px #777; 59 | } 60 | 61 | a img { 62 | border: none; 63 | margin: 0px; 64 | padding: 0px; 65 | } 66 | 67 | #menu { 68 | width: 100%; 69 | padding-top: 5px; 70 | padding-bottom: 5px; 71 | border-top: 2px dotted #9E5454; 72 | border-bottom: 2px dotted #9E5454; 73 | text-align: center; 74 | } 75 | 76 | #menu ul { 77 | margin-left: auto; 78 | margin-right: auto; 79 | padding: 0px; 80 | margin: 0px; 81 | list-style-type: none; 82 | display: block; 83 | } 84 | 85 | #menu ul li { 86 | padding-right: 10px; 87 | padding-left: 10px; 88 | position: relative; 89 | display: inline; 90 | } 91 | 92 | #menu a, #menu a:link, #menu a:visited { 93 | text-decoration: none; 94 | color: #777; 95 | } 96 | 97 | #menu a:hover, #menu a:active { 98 | text-shadow: 0px 0px 4px #9E5454; 99 | } 100 | 101 | #body { 102 | margin: 10px; 103 | } 104 | 105 | pre { 106 | border: 1px solid black; 107 | background: grey; 108 | color: lightgreen; 109 | padding: 5px; 110 | } -------------------------------------------------------------------------------- /lib/statemachine/state.rb: -------------------------------------------------------------------------------- 1 | module Statemachine 2 | 3 | class State #:nodoc: 4 | 5 | attr_reader :id, :statemachine, :superstate 6 | attr_accessor :entry_action, :exit_action 7 | attr_writer :default_transition 8 | 9 | def initialize(id, superstate, state_machine) 10 | @id = id 11 | @superstate = superstate 12 | @statemachine = state_machine 13 | @transitions = {} 14 | @exit_action = @entry_action = nil 15 | end 16 | 17 | def add(transition) 18 | @transitions[transition.event] = transition 19 | end 20 | 21 | def transitions 22 | return @superstate ? @transitions.merge(@superstate.transitions) : @transitions 23 | end 24 | 25 | def non_default_transition_for(event) 26 | transition = @transitions[event] 27 | transition = @superstate.non_default_transition_for(event) if @superstate and not transition 28 | return transition 29 | end 30 | 31 | def default_transition 32 | return @default_transition if @default_transition 33 | return @superstate.default_transition if @superstate 34 | return nil 35 | end 36 | 37 | def transition_for(event) 38 | transition = non_default_transition_for(event) 39 | transition = default_transition if not transition 40 | return transition 41 | end 42 | 43 | def exit(args) 44 | @statemachine.trace("\texiting #{self}") 45 | @statemachine.invoke_action(@exit_action, args, "exit action for #{self}") if @exit_action 46 | @superstate.substate_exiting(self) if @superstate 47 | end 48 | 49 | def enter(args=[]) 50 | @statemachine.trace("\tentering #{self}") 51 | @statemachine.invoke_action(@entry_action, args, "entry action for #{self}") if @entry_action 52 | end 53 | 54 | def activate 55 | @statemachine.state = self 56 | end 57 | 58 | def concrete? 59 | return true 60 | end 61 | 62 | def resolve_startstate 63 | return self 64 | end 65 | 66 | def reset 67 | end 68 | 69 | def to_s 70 | return "'#{id}' state" 71 | end 72 | 73 | end 74 | 75 | end -------------------------------------------------------------------------------- /lib/statemachine/transition.rb: -------------------------------------------------------------------------------- 1 | module Statemachine 2 | 3 | class Transition #:nodoc: 4 | 5 | attr_reader :origin_id, :event, :action 6 | attr_accessor :destination_id 7 | 8 | def initialize(origin_id, destination_id, event, action) 9 | @origin_id = origin_id 10 | @destination_id = destination_id 11 | @event = event 12 | @action = action 13 | end 14 | 15 | def invoke(origin, statemachine, args) 16 | destination = statemachine.get_state(@destination_id) 17 | exits, entries = exits_and_entries(origin, destination) 18 | exits.each { |exited_state| exited_state.exit(args) } 19 | 20 | if @action 21 | result = origin.statemachine.invoke_action(@action, args, "transition action from #{origin} invoked by '#{@event}' event") if @action 22 | transition = !(result === false) 23 | else 24 | transition = true 25 | end 26 | 27 | if transition 28 | terminal_state = entries.last 29 | terminal_state.activate if terminal_state 30 | 31 | entries.each { |entered_state| entered_state.enter(args) } 32 | end 33 | end 34 | 35 | def exits_and_entries(origin, destination) 36 | return [], [] if origin == destination 37 | exits = [] 38 | entries = exits_and_entries_helper(exits, origin, destination) 39 | return exits, entries.reverse 40 | end 41 | 42 | def to_s 43 | return "#{@origin_id} ---#{@event}---> #{@destination_id} : #{action}" 44 | end 45 | 46 | private 47 | 48 | def exits_and_entries_helper(exits, exit_state, destination) 49 | entries = entries_to_destination(exit_state, destination) 50 | return entries if entries 51 | return [] if exit_state == nil 52 | 53 | exits << exit_state 54 | exits_and_entries_helper(exits, exit_state.superstate, destination) 55 | end 56 | 57 | def entries_to_destination(exit_state, destination) 58 | return nil if destination.nil? 59 | entries = [] 60 | state = destination.resolve_startstate 61 | while state 62 | entries << state 63 | return entries if exit_state == state.superstate 64 | state = state.superstate 65 | end 66 | return nil 67 | end 68 | 69 | end 70 | 71 | end 72 | -------------------------------------------------------------------------------- /rails_plugin/vendor/plugins/statemachine/lib/controller_support.rb: -------------------------------------------------------------------------------- 1 | require 'statemachine' 2 | 3 | module Statemachine 4 | module ControllerSupport 5 | 6 | def self.included(base) 7 | base.extend SupportMacro 8 | end 9 | 10 | module SupportMacro 11 | 12 | def supported_by_statemachine(a_context_class, a_statemachine_creation) 13 | self.extend ClassMethods 14 | self.send(:include, InstanceMethods) 15 | 16 | self.context_class = a_context_class 17 | self.statemachine_creation = a_statemachine_creation 18 | end 19 | 20 | end 21 | 22 | module ClassMethods 23 | 24 | attr_accessor :context_class, :statemachine_creation 25 | 26 | end 27 | 28 | module InstanceMethods 29 | 30 | attr_reader :statemachine, :context 31 | 32 | def event 33 | can_continue = before_event 34 | if can_continue 35 | recall_state 36 | puts "@context_event1: #{@context}" 37 | event = params[:event] 38 | arg = params[:arg] 39 | @statemachine.process_event(event, arg) 40 | after_event 41 | save_state 42 | puts "@context_event2: #{@context}" 43 | end 44 | end 45 | 46 | protected 47 | 48 | def before_event 49 | return true 50 | end 51 | 52 | def after_event 53 | end 54 | 55 | def new_context(*args) 56 | @context = self.class.context_class.new 57 | puts "@context_new_context: #{@context}" 58 | @statemachine = self.class.statemachine_creation.call(*args) 59 | @statemachine.context = @context 60 | @context.statemachine = @statemachine 61 | initialize_context(*args) 62 | save_state 63 | end 64 | 65 | def save_state 66 | session[state_session_key] = @context 67 | end 68 | 69 | def initialize_context(*args) 70 | end 71 | 72 | def prepare_for_render 73 | end 74 | 75 | def recall_state 76 | @context = session[state_session_key] 77 | @statemachine = @context.statemachine 78 | end 79 | 80 | def state_session_key 81 | return "#{self.class.name.downcase}_state".to_sym 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /spec/sm_turnstile_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/spec_helper") 2 | 3 | describe "Turn Stile" do 4 | include TurnstileStatemachine 5 | 6 | before(:each) do 7 | create_turnstile 8 | end 9 | 10 | it "connections" do 11 | locked_state = @sm.get_state(:locked) 12 | unlocked_state = @sm.get_state(:unlocked) 13 | 14 | locked_state.transitions.length.should equal(2) 15 | unlocked_state.transitions.length.should equal(2) 16 | 17 | check_transition(locked_state.transitions[:coin], :locked, :unlocked, :coin, @unlock) 18 | check_transition(locked_state.transitions[:pass], :locked, :locked, :pass, @alarm) 19 | check_transition(unlocked_state.transitions[:pass], :unlocked, :locked, :pass, @lock) 20 | check_transition(unlocked_state.transitions[:coin], :unlocked, :locked, :coin, @thankyou) 21 | end 22 | 23 | it "start state" do 24 | @sm.reset 25 | @sm.startstate.should equal(:locked) 26 | @sm.state.should equal(:locked) 27 | end 28 | 29 | it "bad event" do 30 | begin 31 | @sm.process_event(:blah) 32 | self.should.fail_with_message("Exception expected") 33 | rescue Exception => e 34 | e.class.should equal(Statemachine::TransitionMissingException) 35 | e.to_s.should eql("'locked' state does not respond to the 'blah' event.") 36 | end 37 | end 38 | 39 | it "locked state with a coin" do 40 | @sm.process_event(:coin) 41 | 42 | @sm.state.should equal(:unlocked) 43 | @locked.should equal(false) 44 | end 45 | 46 | it "locked state with pass event" do 47 | @sm.process_event(:pass) 48 | 49 | @sm.state.should equal(:locked) 50 | @locked.should equal(true) 51 | @alarm_status.should equal(true) 52 | end 53 | 54 | it "unlocked state with coin" do 55 | @sm.process_event(:coin) 56 | @sm.process_event(:coin) 57 | 58 | @sm.state.should equal(:locked) 59 | @thankyou_status.should equal(true) 60 | end 61 | 62 | it "unlocked state with pass event" do 63 | @sm.process_event(:coin) 64 | @sm.process_event(:pass) 65 | 66 | @sm.state.should equal(:locked) 67 | @locked.should equal(true) 68 | end 69 | 70 | it "events invoked via method_missing" do 71 | @sm.coin 72 | @sm.state.should equal(:unlocked) 73 | @sm.pass 74 | @sm.state.should equal(:locked) 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /rails_plugin/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your web server when you modify this file. 2 | 3 | # Uncomment below to force Rails into production mode when 4 | # you don't control web/app server and can't set it the proper way 5 | # ENV['RAILS_ENV'] ||= 'production' 6 | 7 | # Specifies gem version of Rails to use when vendor/rails is not present 8 | RAILS_GEM_VERSION = '1.2.2' unless defined? RAILS_GEM_VERSION 9 | 10 | # Bootstrap the Rails environment, frameworks, and default configuration 11 | require File.join(File.dirname(__FILE__), 'boot') 12 | 13 | Rails::Initializer.run do |config| 14 | # Settings in config/environments/* take precedence over those specified here 15 | 16 | # Skip frameworks you're not going to use (only works if using vendor/rails) 17 | # config.frameworks -= [ :action_web_service, :action_mailer ] 18 | 19 | # Only load the plugins named here, by default all plugins in vendor/plugins are loaded 20 | # config.plugins = %W( exception_notification ssl_requirement ) 21 | 22 | # Add additional load paths for your own custom dirs 23 | # config.load_paths += %W( #{RAILS_ROOT}/extras ) 24 | 25 | # Force all environments to use the same logger level 26 | # (by default production uses :info, the others :debug) 27 | # config.log_level = :debug 28 | 29 | # Use the database for sessions instead of the file system 30 | # (create the session table with 'rake db:sessions:create') 31 | # config.action_controller.session_store = :active_record_store 32 | 33 | # Use SQL instead of Active Record's schema dumper when creating the test database. 34 | # This is necessary if your schema can't be completely dumped by the schema dumper, 35 | # like if you have constraints or database-specific column types 36 | # config.active_record.schema_format = :sql 37 | 38 | # Activate observers that should always be running 39 | # config.active_record.observers = :cacher, :garbage_collector 40 | 41 | # Make Active Record use UTC-base instead of local time 42 | # config.active_record.default_timezone = :utc 43 | 44 | # See Rails::Configuration for more options 45 | end 46 | 47 | # Add new inflection rules using the following format 48 | # (all these examples are active by default): 49 | # Inflector.inflections do |inflect| 50 | # inflect.plural /^(ox)$/i, '\1en' 51 | # inflect.singular /^(ox)en/i, '\1' 52 | # inflect.irregular 'person', 'people' 53 | # inflect.uncountable %w( fish sheep ) 54 | # end 55 | 56 | # Add new mime types for use in respond_to blocks: 57 | # Mime::Type.register "text/richtext", :rtf 58 | # Mime::Type.register "application/x-mobile", :mobile 59 | 60 | # Include your application configuration below -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | $:.unshift('lib') 2 | require 'rubygems' 3 | require 'rubygems/package_task' 4 | require 'rake/clean' 5 | require 'rdoc/task' 6 | require 'rspec/core/rake_task' 7 | require 'statemachine' 8 | require "bundler/gem_tasks" 9 | 10 | PKG_NAME = "statemachine" 11 | PKG_VERSION = Statemachine::VERSION::STRING 12 | PKG_TAG = Statemachine::VERSION::TAG 13 | PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}" 14 | PKG_FILES = FileList[ 15 | '[A-Z]*', 16 | 'lib/**/*.rb', 17 | 'spec/**/*.rb' 18 | ] 19 | 20 | task :default => :spec 21 | 22 | RSpec::Core::RakeTask.new(:spec) 23 | 24 | 25 | WEB_ROOT = File.expand_path('~/Projects/slagyr.github.com/statemachine/') 26 | 27 | desc 'Generate RDoc' 28 | rd = Rake::RDocTask.new do |rdoc| 29 | rdoc.rdoc_dir = "#{WEB_ROOT}/rdoc" 30 | rdoc.options << '--title' << 'Statemachine' << '--line-numbers' << '--inline-source' << '--main' << 'README.rdoc' 31 | rdoc.rdoc_files.include('README.rdoc', 'CHANGES', 'lib/**/*.rb') 32 | end 33 | task :rdoc 34 | 35 | def egrep(pattern) 36 | Dir['**/*.rb'].each do |fn| 37 | count = 0 38 | open(fn) do |f| 39 | while line = f.gets 40 | count += 1 41 | if line =~ pattern 42 | puts "#{fn}:#{count}:#{line}" 43 | end 44 | end 45 | end 46 | end 47 | end 48 | 49 | desc "Look for TODO and FIXME tags in the code" 50 | task :todo do 51 | egrep /(FIXME|TODO|TBD)/ 52 | end 53 | 54 | task :release => [:clobber, :verify_committed, :verify_user, :verify_password, :spec, :publish_packages, :tag, :publish_website, :publish_news] 55 | 56 | desc "Verifies that there is no uncommitted code" 57 | task :verify_committed do 58 | IO.popen('svn stat') do |io| 59 | io.each_line do |line| 60 | raise "\n!!! Do a svn commit first !!!\n\n" if line =~ /^\s*M\s*/ 61 | end 62 | end 63 | end 64 | 65 | desc "Creates a tag in svn" 66 | task :tag do 67 | puts "Creating tag in SVN" 68 | `svn cp svn+ssh://#{ENV['RUBYFORGE_USER']}@rubyforge.org/var/svn/statemachine/trunk svn+ssh://#{ENV['RUBYFORGE_USER']}@rubyforge.org/var/svn/statemachine/tags/#{PKG_VERSION} -m "Tag release #{PKG_TAG}"` 69 | puts "Done!" 70 | end 71 | 72 | desc 'Generate HTML documentation for website' 73 | task :webgen do 74 | system "rm -rf doc/website/out" 75 | system "rm -rf doc/website/webgen.cache" 76 | system "cd doc/website; webgen -v render; cp -rf out/* #{WEB_ROOT}" 77 | end 78 | 79 | desc "Build the website, but do not publish it" 80 | task :website => [:webgen, :rdoc] 81 | 82 | task :verify_user do 83 | raise "RUBYFORGE_USER environment variable not set!" unless ENV['RUBYFORGE_USER'] 84 | end 85 | 86 | task :verify_password do 87 | raise "RUBYFORGE_PASSWORD environment variable not set!" unless ENV['RUBYFORGE_PASSWORD'] 88 | end 89 | -------------------------------------------------------------------------------- /rails_plugin/app/models/vending_machine_interface.rb: -------------------------------------------------------------------------------- 1 | class VendingMachineInterface 2 | 3 | include Statemachine::ContextSupport 4 | 5 | attr_reader :amount_tendered, :accepting_money, :dispensed_item, :change 6 | attr_accessor :vending_machine 7 | 8 | def initialize 9 | @amount_tendered = 0 10 | @accepting_money = true 11 | end 12 | 13 | def message 14 | if @amount_tendered <= 0 15 | return "Insert Money" 16 | elsif not @accepting_money 17 | return "Select Item" 18 | else 19 | return sprintf("$%.2f", @amount_tendered/100.0) 20 | end 21 | end 22 | 23 | def affordable_items 24 | return @vending_machine.products.reject { |product| product.sold_out? or product.price > @amount_tendered } 25 | end 26 | 27 | def non_affordable_items 28 | return @vending_machine.products.reject { |product| product.sold_out? or product.price <= @amount_tendered } 29 | end 30 | 31 | def sold_out_items 32 | return @vending_machine.products.reject { |product| !product.sold_out? } 33 | end 34 | 35 | def add_dollar 36 | @amount_tendered = @amount_tendered + 100 37 | end 38 | 39 | def add_quarter 40 | @amount_tendered = @amount_tendered + 25 41 | end 42 | 43 | def add_dime 44 | @amount_tendered = @amount_tendered + 10 45 | end 46 | 47 | def add_nickel 48 | @amount_tendered = @amount_tendered + 5 49 | end 50 | 51 | def check_max_price 52 | if @amount_tendered >= @vending_machine.max_price 53 | @statemachine.process_event(:reached_max_price) 54 | end 55 | end 56 | 57 | def accept_money 58 | @accepting_money = true 59 | end 60 | 61 | def refuse_money 62 | @accepting_money = false 63 | end 64 | 65 | def load_product(id) 66 | @selected_product = @vending_machine.product_with_id(id) 67 | end 68 | 69 | def check_affordability 70 | if @amount_tendered >= @selected_product.price and @selected_product.in_stock? 71 | @statemachine.accept_purchase 72 | else 73 | @statemachine.refuse_purchase 74 | end 75 | end 76 | 77 | def make_sale 78 | @dispensed_item = @selected_product 79 | change_pennies = @amount_tendered - @selected_product.price 80 | @change = sprintf("$%.2f", change_pennies/100.0) 81 | @amount_tendered = 0 82 | @selected_product.sold 83 | @vending_machine.add_cash @selected_product.price 84 | @accepting_money = true 85 | end 86 | 87 | def dispense_change 88 | @change = sprintf("$%.2f", @amount_tendered/100.0) 89 | @amount_tendered = 0 90 | end 91 | 92 | def clear_dispensers 93 | @dispensed_item = nil 94 | @change = nil 95 | end 96 | 97 | def load_and_make_sale(id) 98 | load_product(id) 99 | make_sale 100 | end 101 | end -------------------------------------------------------------------------------- /doc/website/src/example3.page: -------------------------------------------------------------------------------- 1 | --- 2 | title: Statemachine Example 3 3 | inMenu: true 4 | directoryName: 5 | --- 6 |

Conditional Logic

7 | 8 | Out vending machine is a good example of when we may need conditional logic. When ever a coin is inserted, the invoked event will depend on whether the total amount of money inserted is sufficient to buy something. If enough money has been tendered, the display should suggest that the customer make a selection. If insufficient money has been inserted, the customer should be prompted to insert more. 9 | 10 | Conditional logic can be accomplished by using entry actions. See the diagram below. 11 | 12 | 13 |
State Diagram with Conditional Logic 14 | 15 | Starting in the Accept Money state, when a coin is inserted, the coin event is fired and the statemachine transitions into the Coin Inserted state. This is where it gets fun. Upon entering of the Coin Inserted state its entry event is invoked: count_amount_tendered. This method will count the money and invoke the not_paid_yet or paid event accordingly. This will cause the statemachine to transition into the appropriate state. 16 | 17 | The Coin Inserted state is unique. You wouldn’t expect to find the statemachine in the Coin Inserted state for any reason except to make this decision. Once the decision is made, the state changes. States like this are called Decision States. 18 | 19 |

Code

20 | 21 |
require 'rubygems'
22 | require 'statemachine'
23 | 
24 | class VendingMachineContext
25 | 
26 |   attr_accessor :statemachine
27 | 
28 |   def initialize
29 |     @amount_tendered = 0
30 |   end
31 | 
32 |   def add_coin
33 |     @amount_tendered = @amount_tendered + 25
34 |   end
35 | 
36 |   def count_amount_tendered
37 |     if @amount_tendered >= 100
38 |       @statemachine.paid
39 |     else
40 |       @statemachine.not_paid_yet
41 |     end
42 |   end
43 | 
44 |   def prompt_money
45 |     puts "$.#{@amount_tendered}: more money please"
46 |   end
47 | 
48 |   def prompt_selection
49 |     puts "please make a selection"
50 |   end
51 | end
52 | 
53 | vending_machine = Statemachine.build do
54 |   trans :accept_money, :coin, :coin_inserted, :add_coin
55 |   state :coin_inserted do
56 |     event :not_paid_yet, :accept_money, :prompt_money
57 |     event :paid, :await_selection, :prompt_selection
58 |     on_entry :count_amount_tendered
59 |   end
60 |   context VendingMachineContext.new
61 | end
62 | vending_machine.context.statemachine = vending_machine
63 | 
64 | vending_machine.coin
65 | vending_machine.coin
66 | vending_machine.coin
67 | vending_machine.coin
68 | 69 | Output: 70 | 71 |
$.25: more money please
72 | $.50: more money please
73 | $.75: more money please
74 | please make a selection
75 | 76 | Moving on to Example 4, we will begin to deal with superstates. 77 | -------------------------------------------------------------------------------- /rails_plugin/spec/plugins/controller_support_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | class SampleContext 4 | include Statemachine::ContextSupport 5 | end 6 | 7 | class SampleStatemachine 8 | attr_accessor :context 9 | end 10 | 11 | class SampleController 12 | 13 | include Statemachine::ControllerSupport 14 | supported_by_statemachine SampleContext, lambda { SampleStatemachine.new } 15 | 16 | attr_accessor :session, :initialized, :before, :after, :params, :can_continue 17 | 18 | def initialize(statemachine, context) 19 | @statemachine = statemachine 20 | @context = context 21 | @session = {} 22 | @can_continue = true 23 | end 24 | 25 | def initialize_context(*args) 26 | @initialized = true 27 | end 28 | 29 | def before_event 30 | @before = true 31 | return @can_continue 32 | end 33 | 34 | def after_event 35 | @after = true 36 | end 37 | 38 | end 39 | 40 | describe "State Machine Support" do 41 | 42 | before(:each) do 43 | @statemachine = mock("statemachine") 44 | @context = mock("context") 45 | @controller = SampleController.new(@statemachine, @context) 46 | end 47 | 48 | it "save state" do 49 | @controller.send(:save_state) 50 | 51 | @controller.session[:samplecontroller_state].should equal @context 52 | end 53 | 54 | it "recall state" do 55 | @controller.session[:samplecontroller_state] = @context 56 | @context.should_receive(:statemachine) 57 | 58 | @controller.send(:recall_state) 59 | end 60 | 61 | it "new context" do 62 | @controller = SampleController.new(nil, nil) 63 | @controller.send(:new_context) 64 | 65 | @controller.context.should_not equal nil 66 | @controller.statemachine.should_not equal nil 67 | @controller.context.statemachine.should equal @controller.statemachine 68 | @controller.statemachine.context.should equal @controller.context 69 | @controller.initialized.should equal true 70 | end 71 | 72 | it "event action" do 73 | @controller.params = {:event => "boo"} 74 | @controller.session[:samplecontroller_state] = @context 75 | @context.should_receive(:statemachine).and_return(@statemachine) 76 | @statemachine.should_receive(:process_event).with("boo", nil) 77 | 78 | @controller.event 79 | 80 | @controller.before.should equal true 81 | @controller.after.should equal true 82 | end 83 | 84 | it "the event is not invoked is before_event return false" do 85 | @controller.params = {:event => "boo"} 86 | @controller.session[:samplecontroller_state] = @context 87 | @context.should_not_receive(:statemachine) 88 | @statemachine.should_not_receive(:process_event) 89 | @controller.can_continue = false 90 | 91 | @controller.event 92 | 93 | @controller.before.should equal true 94 | @controller.after.should_not equal true 95 | end 96 | 97 | 98 | end 99 | -------------------------------------------------------------------------------- /spec/history_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/spec_helper") 2 | 3 | describe "History States" do 4 | 5 | before(:each) do 6 | @sm = Statemachine.build do 7 | superstate :operate do 8 | trans :on, :toggle, :off 9 | trans :off, :toggle, :on 10 | event :fiddle, :middle 11 | end 12 | trans :middle, :fiddle, :operate_H 13 | trans :middle, :dream, :on_H 14 | trans :middle, :faddle, :on 15 | startstate :middle 16 | end 17 | end 18 | 19 | it "no history allowed for concrete states" do 20 | lambda { 21 | @sm.dream 22 | }.should raise_error(Statemachine::StatemachineException, "No history exists for 'on' state since it is not a super state.") 23 | end 24 | 25 | it "error when trying to use history that doesn't exist yet" do 26 | lambda { 27 | @sm.fiddle 28 | }.should raise_error(Statemachine::StatemachineException, "'operate' superstate doesn't have any history yet.") 29 | end 30 | 31 | it "reseting the statemachine resets history" do 32 | @sm.faddle 33 | @sm.fiddle 34 | @sm.get_state(:operate).history_id.should eql(:on) 35 | 36 | @sm.reset 37 | @sm.get_state(:operate).history_id.should eql(nil) 38 | end 39 | 40 | end 41 | 42 | describe "History Default" do 43 | 44 | before(:each) do 45 | @sm = Statemachine.build do 46 | superstate :operate do 47 | trans :on, :toggle, :off 48 | trans :off, :toggle, :on 49 | event :fiddle, :middle 50 | default_history :on 51 | end 52 | trans :middle, :fiddle, :operate_H 53 | startstate :middle 54 | trans :middle, :faddle, :on 55 | end 56 | end 57 | 58 | it "default history" do 59 | @sm.fiddle 60 | @sm.state.should eql(:on) 61 | end 62 | 63 | it "reseting the statemachine resets history" do 64 | @sm.faddle 65 | @sm.toggle 66 | @sm.fiddle 67 | @sm.get_state(:operate).history_id.should eql(:off) 68 | 69 | @sm.reset 70 | @sm.get_state(:operate).history_id.should eql(:on) 71 | end 72 | 73 | end 74 | 75 | describe "Nested Superstates" do 76 | 77 | before(:each) do 78 | @sm = Statemachine.build do 79 | 80 | superstate :grandpa do 81 | trans :start, :go, :daughter 82 | event :sister, :great_auntie 83 | 84 | superstate :papa do 85 | state :son 86 | state :daughter 87 | end 88 | end 89 | 90 | state :great_auntie do 91 | event :foo, :grandpa_H 92 | end 93 | 94 | end 95 | end 96 | 97 | it "should use history of sub superstates when transitioning itto it's own history" do 98 | @sm.go 99 | @sm.sister 100 | @sm.foo 101 | 102 | @sm.state.should eql(:daughter) 103 | end 104 | 105 | end 106 | 107 | 108 | -------------------------------------------------------------------------------- /spec/default_transition_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/spec_helper") 2 | 3 | describe "Default Transition" do 4 | 5 | before(:each) do 6 | @sm = Statemachine.build do 7 | trans :default_state, :start, :test_state 8 | 9 | state :test_state do 10 | default :default_state 11 | end 12 | end 13 | end 14 | 15 | it "the default transition is set" do 16 | test_state = @sm.get_state(:test_state) 17 | test_state.default_transition.should_not be(nil) 18 | test_state.transition_for(:fake_event).should_not be(nil) 19 | end 20 | 21 | it "Should go to the default_state with any event" do 22 | @sm.start 23 | @sm.fake_event 24 | 25 | @sm.state.should eql(:default_state) 26 | end 27 | 28 | it "default transition can have actions" do 29 | me = self 30 | @sm = Statemachine.build do 31 | trans :default_state, :start, :test_state 32 | 33 | state :test_state do 34 | default :default_state, :hi 35 | end 36 | context me 37 | end 38 | 39 | @sm.start 40 | @sm.blah 41 | 42 | @sm.state.should eql(:default_state) 43 | @hi.should eql(true) 44 | end 45 | 46 | def hi 47 | @hi = true 48 | end 49 | 50 | it "superstate supports the default" do 51 | @sm = Statemachine.build do 52 | superstate :test_superstate do 53 | default :default_state 54 | 55 | state :start_state 56 | state :default_state 57 | end 58 | 59 | startstate :start_state 60 | end 61 | 62 | @sm.blah 63 | @sm.state.should eql(:default_state) 64 | end 65 | 66 | it "superstate transitions do not go to the default state" do 67 | @sm = Statemachine.build do 68 | superstate :test_superstate do 69 | event :not_default, :not_default_state 70 | 71 | state :start_state do 72 | default :default_state 73 | end 74 | 75 | state :default_state 76 | end 77 | 78 | startstate :start_state 79 | end 80 | 81 | @sm.state = :start_state 82 | @sm.not_default 83 | @sm.state.should eql(:not_default_state) 84 | end 85 | 86 | it "should use not use superstate's default before using it's own default" do 87 | @sm = Statemachine.build do 88 | superstate :super do 89 | default :super_default 90 | state :base do 91 | default :base_default 92 | end 93 | end 94 | state :super_default 95 | state :base_default 96 | startstate :base 97 | end 98 | 99 | @sm.blah 100 | @sm.state.should eql(:base_default) 101 | end 102 | 103 | it "should be marshalable" do 104 | dump = Marshal.dump(@sm) 105 | loaded = Marshal.load(dump) 106 | loaded.state.should eql(:default_state) 107 | end 108 | 109 | 110 | 111 | end 112 | -------------------------------------------------------------------------------- /doc/website/src/documentation.page: -------------------------------------------------------------------------------- 1 | --- 2 | title: Statemachine Documentation 3 | inMenu: true 4 | directoryName: 5 | --- 6 |

What is a statemachine?

7 | 8 | A statemachine keeps track of the status(state) of an application or device and responds to different inputs, which alter the state of the machine. 9 | 10 |

States, Transitions, and Events

11 | 12 | * State: This is the status of the device or application the statemachine is being used for. At any given time, the statemachine is in one of its predefined states. 13 | * Transition: Moving from one state to another is called a transition. Transitions are invoked by Events. 14 | * Event: Events are the inputs to a statemachine. 15 | 16 | In the Statemachine project, a statemachine is defined by its transitions. 17 | 18 | Take a look at Example 1 to see States, Transitions, and Events being used. 19 | 20 |

Actions

21 | 22 | Actions allow statemachines to perform operations at various point during execution. There are two models for incorporating actions into statemachines. 23 | 24 | * Mealy: A Mealy machine performs actions on transitions. Each transition in a statemachine may invoke a unique action. 25 | * Moore: A Moore machine performs actions when entering a state. Each state may have it’s own entry action. 26 | 27 | Mealy and Moore machines each have advantages and disadvantages. But one great advantage of both it that they are not mutually exclusive. If we use both models, and toss in some exit actions, we’ve got it made! 28 | 29 | Take a look at Example 2 to see Actions being used. 30 | 31 |

Conditional Logic

32 | 33 | If you’re doing any significant amount of work with statemachines, you will most certainly encounter some conditional logic in your statemachines. You may need the state machine to go to one state if a certain condition is true and a different state if it is false. To accomplish this, you can have a state check for the condition and invoke the appropriate transition in an entry action. 34 | 35 | Take a look at Example 3 to see Conditional Logic being used. 36 | 37 |

Superstates

38 | 39 | Oftentimes duplication can arise within a statemachine. One way to solve this problem is through the use of superstates. A superstate is a state that contains other states. One statemachine may have multiple superstates. And every superstate may contain other superstates. ie. Superstates can be nested. 40 | 41 |

History State

42 | 43 | One problem with superstates is that they may not know which state to return to when coming into that state. To solve this problem, superstates come with the history state. Every superstate will remember which state it is in before the superstate is exited. This memory is stored in a pseudo state called the history state. Transitions that end in the history state will recall the last active state of the superstate and enter it. 44 | 45 | Take a look at Example 4 to see Superstates and History States being used. -------------------------------------------------------------------------------- /rails_plugin/public/stylesheets/layout.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0px; 3 | background-image: url('../images/bricks.jpg'); 4 | font-family: "Lucida Grande", "Bitstream Vera Sans", "Verdana"; 5 | font-size: 13px; 6 | } 7 | 8 | #vending_machine_body { 9 | position: absolute; 10 | top: 0px; 11 | left: 50px; 12 | z-index: 1; 13 | } 14 | 15 | #money_panel { 16 | position: absolute; 17 | top: 100px; 18 | left: 353px; 19 | z-index: 2; 20 | } 21 | 22 | #cash_release_button { 23 | position: absolute; 24 | top: 197px; 25 | left: 442px; 26 | border: none; 27 | z-index: 3; 28 | } 29 | 30 | #quartz_screen { 31 | position: absolute; 32 | top: 115px; 33 | left: 359px; 34 | width: 115px; 35 | height: 32px; 36 | text-align: right; 37 | font-size: 12pt; 38 | color: #64F847; 39 | z-index: 100; 40 | } 41 | 42 | #dispenser { 43 | position: absolute; 44 | top: 577px; 45 | left: 133px; 46 | width: 142px; 47 | height: 70px; 48 | text-align: center; 49 | font-size: 12pt; 50 | z-index: 100; 51 | } 52 | 53 | #dispenser p { 54 | border: 1px solid red; 55 | margin: 20px 10px 0px 10px; 56 | padding: 5px; 57 | background-color: black; 58 | color: red; 59 | } 60 | 61 | #product_list { 62 | position: absolute; 63 | top: 235px; 64 | left: 355px; 65 | width: 125px; 66 | height: 440px; 67 | text-align: center; 68 | z-index: 100; 69 | } 70 | 71 | #product_list a { 72 | display: block; 73 | margin: 5px; 74 | padding: 5px; 75 | color: black; 76 | text-decoration: none; 77 | } 78 | 79 | #product_list a:hover { 80 | color: blue; 81 | background-color: grey; 82 | } 83 | 84 | 85 | .non_affordable { 86 | border: 1px solid black; 87 | background-color: lightgrey; 88 | } 89 | 90 | .affordable { 91 | border: 1px solid green; 92 | background-color: lightgreen; 93 | } 94 | 95 | #info { 96 | position: absolute; 97 | top: 275px; 98 | left: 525px; 99 | width: 250px; 100 | padding: 10px; 101 | border: 1px solid black; 102 | z-index: 100; 103 | } 104 | 105 | #cash { 106 | position: absolute; 107 | top: 50px; 108 | left: 525px; 109 | width: 250px; 110 | padding: 10px; 111 | border: 1px solid black; 112 | text-align: center; 113 | z-index: 100; 114 | } 115 | 116 | #links { 117 | position: absolute; 118 | top: 25px; 119 | height: 20px; 120 | left: 525px; 121 | width: 250px; 122 | text-align: center; 123 | z-index: 2; 124 | } 125 | 126 | h3 { 127 | text-align: center; 128 | margin: 5px; 129 | font-size: 20pt; 130 | } 131 | 132 | .money_active { 133 | background-color: yellow; 134 | } 135 | 136 | #change { 137 | position: absolute; 138 | top: 681px; 139 | left: 362px; 140 | z-index: 2; 141 | } 142 | 143 | #change_amount { 144 | position: absolute; 145 | top: 681px; 146 | left: 362px; 147 | width: 109px; 148 | height: 74px; 149 | z-index: 100; 150 | text-align: center; 151 | } 152 | 153 | #change_amount p { 154 | border: 1px solid #64F847; 155 | margin: 20px 10px 0px 10px; 156 | padding: 5px; 157 | background-color: black; 158 | color: #64F847; 159 | } 160 | 161 | th { 162 | text-align: left; 163 | } 164 | -------------------------------------------------------------------------------- /spec/sm_odds_n_ends_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/spec_helper") 2 | 3 | describe "State Machine Odds And Ends" do 4 | include SwitchStatemachine 5 | 6 | before(:each) do 7 | create_switch 8 | end 9 | 10 | it "method missing delegates to super in case of no event" do 11 | $method_missing_called = false 12 | module Blah 13 | def method_missing(message, *args) 14 | $method_missing_called = true 15 | end 16 | end 17 | @sm.extend(Blah) 18 | @sm.blah 19 | $method_missing_called.should eql(true) 20 | end 21 | 22 | it "should raise TransistionMissingException when the state doesn't respond to the event" do 23 | lambda { @sm.blah }.should raise_error(Statemachine::TransitionMissingException, "'off' state does not respond to the 'blah' event.") 24 | end 25 | 26 | it "should respond to valid events" do 27 | @sm.respond_to?(:toggle).should eql(true) 28 | @sm.respond_to?(:blah).should eql(false) 29 | end 30 | 31 | it "should not crash when respond_to? called when the statemachine is not in a state" do 32 | @sm.instance_eval { @state = nil } 33 | lambda { @sm.respond_to?(:toggle) }.should_not raise_error 34 | @sm.respond_to?(:toggle).should eql(false) 35 | end 36 | 37 | it "set state with string" do 38 | @sm.state.should equal(:off) 39 | @sm.state = "on" 40 | @sm.state.should equal(:on) 41 | end 42 | 43 | it "set state with symbol" do 44 | @sm.state.should equal(:off) 45 | @sm.state = :on 46 | @sm.state.should equal(:on) 47 | end 48 | 49 | it "process event accepts strings" do 50 | @sm.process_event("toggle") 51 | @sm.state.should equal(:on) 52 | end 53 | 54 | it "states without transitions are valid" do 55 | @sm = Statemachine.build do 56 | trans :middle, :push, :stuck 57 | startstate :middle 58 | end 59 | 60 | @sm.push 61 | @sm.state.should equal(:stuck) 62 | end 63 | 64 | it "traces output with name" do 65 | io = StringIO.new 66 | @sm.name = "Switch" 67 | @sm.tracer = io 68 | @sm.toggle 69 | @sm.toggle 70 | 71 | expected = StringIO.new 72 | expected.puts "(Switch) Event: toggle" 73 | expected.puts "(Switch) \texiting 'off' state" 74 | expected.puts "(Switch) \tentering 'on' state" 75 | expected.puts "(Switch) Event: toggle" 76 | expected.puts "(Switch) \texiting 'on' state" 77 | expected.puts "(Switch) \tentering 'off' state" 78 | 79 | io.string.should == expected.string 80 | end 81 | 82 | it "traces output without name" do 83 | io = StringIO.new 84 | @sm.tracer = io 85 | @sm.toggle 86 | @sm.toggle 87 | 88 | expected = StringIO.new 89 | expected.puts "Event: toggle" 90 | expected.puts "\texiting 'off' state" 91 | expected.puts "\tentering 'on' state" 92 | expected.puts "Event: toggle" 93 | expected.puts "\texiting 'on' state" 94 | expected.puts "\tentering 'off' state" 95 | 96 | io.string.should == expected.string 97 | end 98 | 99 | end 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /spec/sm_entry_exit_actions_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/spec_helper") 2 | 3 | describe "State Machine Entry and Exit Actions" do 4 | 5 | before(:each) do 6 | @log = [] 7 | @sm = Statemachine.build do 8 | trans :off, :toggle, :on, Proc.new { @log << "on" } 9 | trans :on, :toggle, :off, Proc.new { @log << "off" } 10 | end 11 | @sm.context = self 12 | end 13 | 14 | it "entry action" do 15 | @sm.get_state(:on).entry_action = Proc.new { @log << "entered_on" } 16 | 17 | @sm.toggle 18 | 19 | @log.join(",").should eql("on,entered_on") 20 | end 21 | 22 | it "exit action" do 23 | @sm.get_state(:off).exit_action = Proc.new { @log << "exited_off" } 24 | 25 | @sm.toggle 26 | 27 | @log.join(",").should eql("exited_off,on") 28 | end 29 | 30 | it "exit and entry" do 31 | @sm.get_state(:off).exit_action = Proc.new { @log << "exited_off" } 32 | @sm.get_state(:on).entry_action = Proc.new { @log << "entered_on" } 33 | 34 | @sm.toggle 35 | 36 | @log.join(",").should eql("exited_off,on,entered_on") 37 | end 38 | 39 | it "entry and exit actions may be parameterized" do 40 | @sm.get_state(:off).exit_action = Proc.new { |a| @log << "exited_off(#{a})" } 41 | @sm.get_state(:on).entry_action = Proc.new { |a, b| @log << "entered_on(#{a},#{b})" } 42 | 43 | @sm.toggle "one", "two" 44 | 45 | @log.join(",").should eql("exited_off(one),on,entered_on(one,two)") 46 | end 47 | 48 | it "current state is set prior to exit and entry actions" do 49 | @sm.get_state(:off).exit_action = Proc.new { @log << @sm.state } 50 | @sm.get_state(:on).entry_action = Proc.new { @log << @sm.state } 51 | 52 | @sm.toggle 53 | 54 | @log.join(",").should eql("off,on,on") 55 | end 56 | 57 | it "current state is set prior to exit and entry actions even with super states" do 58 | @sm = Statemachine::Statemachine.new 59 | Statemachine.build(@sm) do 60 | superstate :off_super do 61 | on_exit Proc.new {@log << @sm.state} 62 | state :off 63 | event :toggle, :on, Proc.new { @log << "super_on" } 64 | end 65 | superstate :on_super do 66 | on_entry Proc.new { @log << @sm.state } 67 | state :on 68 | event :toggle, :off, Proc.new { @log << "super_off" } 69 | end 70 | startstate :off 71 | end 72 | @sm.context = self 73 | 74 | @sm.toggle 75 | @log.join(",").should eql("off,super_on,on") 76 | end 77 | 78 | it "entry actions invokes another event" do 79 | @sm.get_state(:on).entry_action = Proc.new { @sm.toggle } 80 | 81 | @sm.toggle 82 | @log.join(",").should eql("on,off") 83 | @sm.state.should equal(:off) 84 | end 85 | 86 | it "startstate's entry action should be called when the statemachine starts" do 87 | the_context = self 88 | @sm = Statemachine.build do 89 | trans :a, :b, :c 90 | on_entry_of :a, Proc.new { @log << "entering a" } 91 | context the_context 92 | end 93 | 94 | @log.join(",").should eql("entering a") 95 | end 96 | 97 | 98 | 99 | end 100 | -------------------------------------------------------------------------------- /doc/website/src/example4.page: -------------------------------------------------------------------------------- 1 | --- 2 | title: Statemachine Example 4 3 | inMenu: true 4 | directoryName: 5 | --- 6 |

Superstates

7 | 8 | Often in statemachines, duplication can arise. For example, the vending machine in our examples may need periodic repairs. It’s not certain which state the vending machine will be in when the repair man arrives. So all states should have a transition into the Repair Mode state. 9 | 10 | 11 |
Diagram 1 - Without Superstates 12 | 13 | In this diagram, both the Waiting and Paid states have a transition to the Repair Mode invoked by the repair event. Duplication! We can dry this up by using the Superstate construct. See below: 14 | 15 | 16 |
Diagram 2 - With Superstates 17 | 18 | Here we introduce the Operational superstate. Both the Waiting and Paid states are contained within the superstate which implies that they inherit all of the superstate’s transitions. That means we only need one transition into the Repair Mode state from the Operational superstate to achieve the same behavior as the solution in diagram 1. 19 | 20 | One statemachine may have multiple superstates. And every superstate may contain other superstates. ie. Superstates can be nested. 21 | 22 |

History State

23 | 24 | The solution in diagram 2 has an advantage over diagram 1. In diagram 1, once the repair man is done he triggers the operate event and the vending machine transitions into the Waiting event. This is unfortunate. Even if the vending machine was in the Paid state before the repair man came along, it will be in the Waiting state after he leaves. Shouldn’t it go back into the Paid state? 25 | 26 | This is where use of the history state is useful. You can see the history state being use in diagram 2. In this solution, the history state allows the vending machine to return from a repair session into the same state it was in before, as though nothing happened at all. 27 | 28 |

Code

29 | 30 | The following code builds the statemachine in diagram 2. Watch out for the _H. This is how the history state is denoted. If you have a superstate named foo, then it’s history state will be named foo_H. 31 | 32 |
require 'rubygems'
33 | require 'statemachine'
34 | 
35 | vending_machine = Statemachine.build do
36 |   superstate :operational do
37 |     trans :waiting, :dollar, :paid
38 |     trans :paid, :selection, :waiting
39 |     trans :waiting, :selection, :waiting
40 |     trans :paid, :dollar, :paid
41 | 
42 |     event :repair, :repair_mode,  Proc.new { puts "Entering Repair Mode" }
43 |   end
44 | 
45 |   trans :repair_mode, :operate, :operational_H, Proc.new { puts "Exiting Repair Mode" }
46 | 
47 |   on_entry_of :waiting, Proc.new { puts "Entering Waiting State" }
48 |   on_entry_of :paid, Proc.new { puts "Entering Paid State" }
49 | end
50 | 
51 | vending_machine.repair
52 | vending_machine.operate
53 | vending_machine.dollar
54 | vending_machine.repair
55 | vending_machine.operate
56 | 57 | Output: 58 | 59 |
Entering Repair Mode
60 | Exiting Repair Mode
61 | Entering Waiting State
62 | Entering Paid State
63 | Entering Repair Mode
64 | Exiting Repair Mode
65 | Entering Paid State
-------------------------------------------------------------------------------- /rails_plugin/spec/models/vending_statemachine_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe "Vending Statemachine" do 4 | 5 | before(:each) do 6 | @sm = VendingStatemachine.statemachine 7 | @context = mock("context") 8 | @sm.context = @context 9 | end 10 | 11 | it "start state" do 12 | @sm.state.should equal :standby 13 | end 14 | 15 | it "dollar" do 16 | check_money_event(:dollar, :add_dollar) 17 | end 18 | 19 | it "quarter" do 20 | check_money_event(:quarter, :add_quarter) 21 | end 22 | 23 | it "dime" do 24 | check_money_event(:dime, :add_dime) 25 | end 26 | 27 | it "nickel" do 28 | check_money_event(:nickel, :add_nickel) 29 | end 30 | 31 | def check_money_event(event, method) 32 | @context.should_receive(:clear_dispensers) 33 | @context.should_receive(method) 34 | @context.should_receive(:check_max_price) 35 | 36 | @sm.process_event(event) 37 | @sm.state.should equal :collecting_money 38 | end 39 | 40 | it "standby selection" do 41 | @context.should_receive(:clear_dispensers) 42 | 43 | @sm.selection 44 | @sm.state.should equal :standby 45 | end 46 | 47 | it "standby collecting_money" do 48 | @sm.state = :collecting_money 49 | @context.should_receive(:load_product).with(123) 50 | @context.should_receive(:check_affordability) 51 | 52 | @sm.selection(123) 53 | @sm.state.should equal :validating_purchase 54 | end 55 | 56 | it "reached max_price" do 57 | @sm.state = :collecting_money 58 | @context.should_receive(:refuse_money) 59 | 60 | @sm.reached_max_price 61 | @sm.state.should equal :max_price_tendered 62 | end 63 | 64 | it "accept purchase" do 65 | @sm.state = :validating_purchase 66 | @context.should_receive(:make_sale) 67 | 68 | @sm.accept_purchase 69 | @sm.state.should equal :standby 70 | end 71 | 72 | it "refuse purchase" do 73 | @sm.state = :validating_purchase 74 | @context.should_receive(:check_max_price) 75 | 76 | @sm.refuse_purchase 77 | @sm.state.should equal :collecting_money 78 | end 79 | 80 | it "money in max_price_tendered" do 81 | @sm.state = :max_price_tendered 82 | 83 | @sm.dollar 84 | @sm.state.should equal :max_price_tendered 85 | @sm.quarter 86 | @sm.state.should equal :max_price_tendered 87 | @sm.dime 88 | @sm.state.should equal :max_price_tendered 89 | @sm.nickel 90 | @sm.state.should equal :max_price_tendered 91 | end 92 | 93 | it "selection in max_price_tendered" do 94 | @sm.state = :max_price_tendered 95 | @context.should_receive(:load_and_make_sale).with(123) 96 | @context.should_receive(:accept_money) 97 | 98 | @sm.selection(123) 99 | @sm.state.should equal :standby 100 | end 101 | 102 | it "release money from standby" do 103 | @context.should_receive(:clear_dispensers) 104 | @sm.release_money 105 | 106 | @sm.state.should equal :standby 107 | end 108 | 109 | it "release money from collecting money" do 110 | @sm.state = :collecting_money 111 | @context.should_receive(:dispense_change) 112 | 113 | @sm.release_money 114 | 115 | @sm.state.should equal :standby 116 | end 117 | 118 | it "release money from max_price_tendered" do 119 | @sm.state = :max_price_tendered 120 | @context.should_receive(:dispense_change) 121 | @context.should_receive(:accept_money) 122 | 123 | @sm.release_money 124 | 125 | @sm.state.should equal :standby 126 | end 127 | 128 | 129 | 130 | end 131 | -------------------------------------------------------------------------------- /doc/website/src/example1.page: -------------------------------------------------------------------------------- 1 | --- 2 | title: Statemachine Example 1 3 | inMenu: true 4 | directoryName: 5 | --- 6 |

States, Transitions, and Events

7 | 8 |

This is a simple statemachine showing use of states and transitions.

9 | 10 | 11 |
The Vending Machine Statemachine Diagram 12 | 13 | Above is a UML diagram of the statemachine the runs a simple vending machine. We can see that there are two rectangles with rounded corners. These are States. The vending machine has two possible states, Waiting and Paid. At any given time during execution, the vending machine will be in one of these states. 14 | 15 | Note the arrows going from one state to another. These arrows represent the transitions of the statemachine. Also note that each transition is labeled with an Event. They invoke transitions. For example, when the vending machine is in the Waiting state and the dollar event is received, the statemachine will transition into the Paid state. When in the paid state and the selection event is received, the statemachine will transition back into the Waiting state. 16 | 17 | This should seem reasonable. Imagine a real vending machine. When you walk up to it it’s waiting for you to put money in. You pay by sticking a dollar in and then you make your selection. After this happy transaction, the vending machine waits for the next client. 18 | 19 | This scenario is not the only possibility though. Statemachine are very helpful in examining all possible flows through the system. Take the Waiting state. We don’t normally expect users to make selections if they haven’t paid but it’s a possibility. As you can see this unexpected event is handled by our vending machine. It will simply continue to wait for your dollar. And it would be foolish for someone to put more money in the the vending machine if they’ve already paid. Foolish or not, you and I know it happens. Our vending machine handles this graciously by taking the money and allowing the user to make a selection for the fist dollar they supplied. Effectively the client loses the extra money they put in. (grin) 20 | 21 | Implementing the Statemachine: 22 | 23 | We have identified 3 fundamental components to a statemachine: States, Transitions, and Events. It turns out that the simplest way to define a statemachine is to define its transitions. Each transition can be defined by identifying the state where it begins, the event by which is invoked, and the state where it ends. Using this scheme we can define out vending machine like so… 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
Origin StateEventDestination State
WaitingdollarPaid
PaidselectionWaiting
WaitingselectionWaiting
PaiddollarPaid
33 | 34 | Defining it in ruby is not much harder: 35 |
require 'rubygems'
36 | require 'statemachine'
37 | 
38 | vending_machine = Statemachine.build do
39 |   trans :waiting, :dollar, :paid
40 |   trans :paid, :selection, :waiting
41 |   trans :waiting, :selection, :waiting
42 |   trans :paid, :dollar, :paid
43 | end
44 | 45 | The above snippet assumes you have the statemachine gem installed. (See Overview for installation instructions). 46 | 47 | The outcome of this code an instance of Statemachine stored in the variable named vending_machine. To use our statemachine we need to send events to it. This is done by calling methods that correspond to events. 48 | 49 |
puts vending_machine.state
50 | vending_machine.dollar
51 | puts vending_machine.state
52 | vending_machine.selection
53 | puts vending_machine.state
54 | That sequence of events will produce the following ouput: 55 |
waiting
56 | paid
57 | waiting
58 | 59 | That’s it for the basics. Example 2 shows how to make our statemachine more functional by adding actions. -------------------------------------------------------------------------------- /spec/transition_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/spec_helper") 2 | 3 | describe "Transition Calculating Exits and Entries" do 4 | 5 | before(:each) do 6 | @transition = Statemachine::Transition.new(nil, nil, nil, nil) 7 | end 8 | 9 | it "to nil" do 10 | @a = Statemachine::State.new("a", nil, nil) 11 | exits, entries = @transition.exits_and_entries(@a, nil) 12 | exits.to_s.should eql([@a].to_s) 13 | entries.to_s.should eql([].to_s) 14 | entries.length.should equal(0) 15 | end 16 | 17 | it "to itself" do 18 | @a = Statemachine::State.new("a", nil, nil) 19 | exits, entries = @transition.exits_and_entries(@a, @a) 20 | exits.should == [] 21 | entries.should == [] 22 | end 23 | 24 | it "to friend" do 25 | @a = Statemachine::State.new("a", nil, nil) 26 | @b = Statemachine::State.new("b", nil, nil) 27 | exits, entries = @transition.exits_and_entries(@a, @b) 28 | exits.to_s.should eql([@a].to_s) 29 | entries.to_s.should eql([@b].to_s) 30 | end 31 | 32 | it "to parent" do 33 | @b = Statemachine::State.new("b", nil, nil) 34 | @a = Statemachine::State.new("a", @b, nil) 35 | exits, entries = @transition.exits_and_entries(@a, @b) 36 | exits.to_s.should eql([@a, @b].to_s) 37 | entries.to_s.should eql([@b].to_s) 38 | end 39 | 40 | it "to uncle" do 41 | @b = Statemachine::State.new("b", nil, nil) 42 | @a = Statemachine::State.new("a", @b, nil) 43 | @c = Statemachine::State.new("c", nil, nil) 44 | exits, entries = @transition.exits_and_entries(@a, @c) 45 | exits.to_s.should eql([@a, @b].to_s) 46 | entries.to_s.should eql([@c].to_s) 47 | end 48 | 49 | it "to cousin" do 50 | @b = Statemachine::State.new("b", nil, nil) 51 | @d = Statemachine::State.new("d", nil, nil) 52 | @a = Statemachine::State.new("a", @b, nil) 53 | @c = Statemachine::State.new("c", @d, nil) 54 | exits, entries = @transition.exits_and_entries(@a, @c) 55 | exits.to_s.should eql([@a, @b].to_s) 56 | entries.to_s.should eql([@d, @c].to_s) 57 | end 58 | 59 | it "to nephew" do 60 | @b = Statemachine::State.new("b", nil, nil) 61 | @c = Statemachine::State.new("c", nil, nil) 62 | @a = Statemachine::State.new("a", @b, nil) 63 | exits, entries = @transition.exits_and_entries(@c, @a) 64 | exits.to_s.should eql([@c].to_s) 65 | entries.to_s.should eql([@b,@a].to_s) 66 | end 67 | 68 | it "to sister" do 69 | @c = Statemachine::State.new("c", nil, nil) 70 | @a = Statemachine::State.new("a", @c, nil) 71 | @b = Statemachine::State.new("b", @c, nil) 72 | exits, entries = @transition.exits_and_entries(@a, @b) 73 | exits.to_s.should eql([@a].to_s) 74 | entries.to_s.should eql([@b].to_s) 75 | end 76 | 77 | it "to second cousin" do 78 | @c = Statemachine::State.new("c", nil, nil) 79 | @b = Statemachine::State.new("b", @c, nil) 80 | @a = Statemachine::State.new("a", @b, nil) 81 | @e = Statemachine::State.new("e", @c, nil) 82 | @d = Statemachine::State.new("d", @e, nil) 83 | exits, entries = @transition.exits_and_entries(@a, @d) 84 | exits.to_s.should eql([@a, @b].to_s) 85 | entries.to_s.should eql([@e, @d].to_s) 86 | end 87 | 88 | it "to grandparent" do 89 | @c = Statemachine::State.new("c", nil, nil) 90 | @b = Statemachine::State.new("b", @c, nil) 91 | @a = Statemachine::State.new("a", @b, nil) 92 | exits, entries = @transition.exits_and_entries(@a, @c) 93 | exits.to_s.should eql([@a, @b, @c].to_s) 94 | entries.to_s.should eql([@c].to_s) 95 | end 96 | 97 | it "to parent's grandchild" do 98 | @c = Statemachine::State.new("c", nil, nil) 99 | @b = Statemachine::State.new("b", @c, nil) 100 | @a = Statemachine::State.new("a", @b, nil) 101 | @d = Statemachine::State.new("d", @c, nil) 102 | exits, entries = @transition.exits_and_entries(@d, @a) 103 | exits.to_s.should eql([@d].to_s) 104 | entries.to_s.should eql([@b, @a].to_s) 105 | end 106 | 107 | end 108 | -------------------------------------------------------------------------------- /spec/sm_action_parameterization_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/spec_helper") 2 | 3 | describe "State Machine Odds And Ends" do 4 | include SwitchStatemachine 5 | 6 | before(:each) do 7 | create_switch 8 | end 9 | 10 | it "action with one parameter" do 11 | Statemachine.build(@sm) { |s| s.trans :off, :set, :on, Proc.new { |value| @status = value } } 12 | @sm.set "blue" 13 | @status.should eql("blue") 14 | @sm.state.should equal(:on) 15 | end 16 | 17 | it "action with two parameters" do 18 | Statemachine.build(@sm) { |s| s.trans :off, :set, :on, Proc.new { |a, b| @status = [a, b].join(",") } } 19 | @sm.set "blue", "green" 20 | @status.should eql("blue,green") 21 | @sm.state.should equal(:on) 22 | end 23 | 24 | it "action with three parameters" do 25 | Statemachine.build(@sm) { |s| s.trans :off, :set, :on, Proc.new { |a, b, c| @status = [a, b, c].join(",") } } 26 | @sm.set "blue", "green", "red" 27 | @status.should eql("blue,green,red") 28 | @sm.state.should equal(:on) 29 | end 30 | 31 | it "action with four parameters" do 32 | Statemachine.build(@sm) { |s| s.trans :off, :set, :on, Proc.new { |a, b, c, d| @status = [a, b, c, d].join(",") } } 33 | @sm.set "blue", "green", "red", "orange" 34 | @status.should eql("blue,green,red,orange") 35 | @sm.state.should equal(:on) 36 | end 37 | 38 | it "action with five parameters" do 39 | Statemachine.build(@sm) { |s| s.trans :off, :set, :on, Proc.new { |a, b, c, d, e| @status = [a, b, c, d, e].join(",") } } 40 | @sm.set "blue", "green", "red", "orange", "yellow" 41 | @status.should eql("blue,green,red,orange,yellow") 42 | @sm.state.should equal(:on) 43 | end 44 | 45 | it "action with six parameters" do 46 | Statemachine.build(@sm) { |s| s.trans :off, :set, :on, Proc.new { |a, b, c, d, e, f| @status = [a, b, c, d, e, f].join(",") } } 47 | @sm.set "blue", "green", "red", "orange", "yellow", "indigo" 48 | @status.should eql("blue,green,red,orange,yellow,indigo") 49 | @sm.state.should equal(:on) 50 | end 51 | 52 | it "action with seven parameters" do 53 | Statemachine.build(@sm) { |s| s.trans :off, :set, :on, Proc.new { |a, b, c, d, e, f, g| @status = [a, b, c, d, e, f, g].join(",") } } 54 | @sm.set "blue", "green", "red", "orange", "yellow", "indigo", "violet" 55 | @status.should eql("blue,green,red,orange,yellow,indigo,violet") 56 | @sm.state.should equal(:on) 57 | end 58 | 59 | it "action with eight parameters" do 60 | Statemachine.build(@sm) { |s| s.trans :off, :set, :on, Proc.new { |a, b, c, d, e, f, g, h| @status = [a, b, c, d, e, f, g, h].join(",") } } 61 | @sm.set "blue", "green", "red", "orange", "yellow", "indigo", "violet", "ultra-violet" 62 | @status.should eql("blue,green,red,orange,yellow,indigo,violet,ultra-violet") 63 | @sm.state.should equal(:on) 64 | end 65 | 66 | it "calling process_event with parameters" do 67 | Statemachine.build(@sm) { |s| s.trans :off, :set, :on, Proc.new { |a, b, c| @status = [a, b, c].join(",") } } 68 | @sm.process_event(:set, "blue", "green", "red") 69 | @status.should eql("blue,green,red") 70 | @sm.state.should equal(:on) 71 | end 72 | 73 | it "Insufficient params" do 74 | Statemachine.build(@sm) { |s| s.trans :off, :set, :on, Proc.new { |a, b, c| @status = [a, b, c].join(",") } } 75 | lambda { @sm.set "blue", "green" }.should raise_error(Statemachine::StatemachineException, 76 | "Insufficient parameters. (transition action from 'off' state invoked by 'set' event)") 77 | end 78 | 79 | it "infinate args" do 80 | Statemachine.build(@sm) { |s| s.trans :off, :set, :on, Proc.new { |*a| @status = a.join(",") } } 81 | @sm.set(1, 2, 3) 82 | @status.should eql("1,2,3") 83 | 84 | @sm.state = :off 85 | @sm.set(1, 2, 3, 4, 5, 6) 86 | @status.should eql("1,2,3,4,5,6") 87 | end 88 | 89 | it "Insufficient params when params are infinate" do 90 | Statemachine.build(@sm) { |s| s.trans :off, :set, :on, Proc.new { |a, *b| @status = a.to_s + ":" + b.join(",") } } 91 | @sm.set(1, 2, 3) 92 | @status.should eql("1:2,3") 93 | 94 | @sm.state = :off 95 | 96 | lambda { @sm.set }.should raise_error(Statemachine::StatemachineException, 97 | "Insufficient parameters. (transition action from 'off' state invoked by 'set' event)") 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | = Statemachine Changelog 2 | 3 | == Version 2.3.0 4 | 5 | * Introduce on_state_change for building persistent states from @godfat (https://github.com/slagyr/statemachine/pull/13) 6 | * Update rake tasks according to it's deprecation messages from @godfat (https://github.com/slagyr/statemachine/pull/14) 7 | * Base Exceptions on StandardError from @Asmod4n (https://github.com/slagyr/statemachine/pull/15) 8 | 9 | == Version 2.2.0 10 | 11 | * applied dotfile improvements from @wsobel (https://github.com/slagyr/statemachine/pull/12) 12 | 13 | == Version 2.1.0 14 | 15 | * Do not perform transitions if context method returns false | by @zombor (https://github.com/slagyr/statemachine/pull/11) 16 | 17 | == Version 2.0.1 18 | 19 | * initialize ivars so that runtimes running with -d don't complain about uninitialized ivars (chuckremes) 20 | * updated to use Bundler and Rspec 2 (ericmeyer) 21 | 22 | == Version 1.1.0 23 | 24 | DotGraph 25 | * DotGraph generator was added to generate graphs of statemachines using Omnigraffle. 26 | * Fixed bug in Java generator where Statenames where not formated correctly. 27 | 28 | == Version 1.0.0 29 | 30 | Generator 31 | * Java generator was added. Statemachines defined in the Ruby DSL can generate Java code. 32 | 33 | == Version 0.4.2 34 | 35 | Simple Fixes 36 | * Fixed respond_to? to handle the, what should be impossible, case when the state is nil 37 | * Switch history members variable to store id rather than object. 38 | 39 | == Version 0.4.1 40 | 41 | Simple Fixes 42 | * Fixed priority of default transitions, self defaults first, then superstate defaults. 43 | 44 | == Version 0.4.0 45 | 46 | Feature enhancements 47 | * enabled nested superstate history 48 | * TransitionMissingException's are raised when the statemachine can't respond to an event 49 | * Statmachine overrides respond_to? to respond to valid events. 50 | 51 | Behavior Fixes 52 | * fixed default transition so that superstate transitions have priority over default 53 | 54 | == Version 0.3.0 55 | 56 | Feature enhancements 57 | * added default transitions 58 | * added default history for superstates 59 | * the context method in the builder will set the context's statemachine variable if the context respond_to?(:statemachine=) 60 | 61 | Behavior Fixes 62 | * the entry action of the startstate is called when the statemachine starts or is reset. 63 | * resetting the statemachine will reset the history state for each superstate. 64 | 65 | == Version 0.2.2 66 | 67 | Minor plugin update 68 | * introduced before_event and after_event hooks for controllers 69 | 70 | == Version 0.2.1 71 | 72 | Rails Plugin. 73 | * Rails plugin introduced 74 | 75 | == Version 0.2.0 76 | 77 | Separation of logic from behavior. 78 | * Prefered builder syntax implemented 79 | * statemachine have a context which defines all the behavior 80 | * startstate can be set at any time in the builder 81 | * states can be declared without blocks 82 | * context can be set in builder 83 | 84 | == Version 0.1.0 85 | 86 | A new way to build the statemachines 87 | * cleaner API for running a statemachine 88 | * much refactoring 89 | * new API for building statemachine 90 | * process_event accepts strings 91 | 92 | == Version 0.0.4 93 | 94 | Some minor improvements 95 | * Proper handling of state transition implemented, such that the proper state is set for entry and exit actions. 96 | * can now use State objects in addition to symbols while creating a transition 97 | * more compliant implementation of history state 98 | 99 | == Version 0.0.3 100 | 101 | Bug fix dealing with entry and exit actions. The state machine's state need to be set to the entered/exited state before calling the 102 | exit/entry action. 103 | * added a couple specs in the exit_entry_spec 104 | * modified state.entered/exited methods to set the state 105 | * modifed the StateMachine.state to accept state objects. 106 | * removed running attribute from StateMachine because it wasn't much use 107 | * also removed the nil (end state) 108 | 109 | == Version 0.0.2 110 | 111 | More conventional file structure 112 | * nothing much to report in terms of changes. 113 | 114 | == Version 0.0.1 115 | 116 | 0.0.0 didn't seem to work as a gem so maybe this one will. 117 | 118 | * nothing really, just playing with rake and release configuration 119 | 120 | == Version 0.0.0 121 | 122 | The first release. Most finite state machine features are implemented 123 | * states 124 | * transitions 125 | * transition actions 126 | * super states 127 | * entry actions 128 | * exit actions 129 | * history state 130 | -------------------------------------------------------------------------------- /doc/website/src/example2.page: -------------------------------------------------------------------------------- 1 | --- 2 | title: Statemachine Example 2 3 | inMenu: true 4 | directoryName: 5 | --- 6 |

Actions

7 | 8 |

This example shows the addition of actions to our statemachine from Example 1.

9 | 10 | The vending machine statemachine had some problems. Adding some actions will solve many of them. Here’s the same statemachine with actions. 11 | 12 | 13 |
The Vending Machine Statemachine Diagram, Version 2 14 | 15 | You can see I’ve added three transition actions (the Mealy type). Check out the transition from Waiting to Paid. When this transition is triggered the activate action will be called which will activate the hardware that dispenses goodies. Also, when a selection is made, transitioning from Paid to Waiting, the release action will cause the hardware to release the selected product. Finally, this version of the vending machine won’t steal your money any more. When an extra dollar is inserted, the refund event is invoked and the dollar is refunded. 16 | 17 | Notice that the Waiting state has an entry action (Moore type) and an exit action. When ever the Waiting states is entered, the sales_mode action is invoked. The intent of this action is to make the vending machine blink or flash or scroll text; whatever it takes to attract customers. When the Waiting state is exited, the vending will go into operation_mode where all the blinking stops so the customer do business. 18 | 19 | Implementation: 20 | 21 | Here’s how the new vending machine can be implemented in Ruby: 22 | 23 |
vending_machine = Statemachine.build do
24 |   state :waiting do
25 |     event :dollar, :paid, :activate
26 |     event :selection, :waiting
27 |     on_entry :sales_mode
28 |     on_exit :operation_mode
29 |   end
30 |   trans :paid, :selection, :waiting, :release
31 |   trans :paid, :dollar, :paid, :refund
32 |   context VendingMachineContext.new
33 | end
34 | 35 | There are several new tricks to learn here. First is the state method. This is the formal syntax for declaring a state. The informal syntax is the trans method which we’ve already seen. The state method requires the state id and an option block. Every method invoked within the block is applied to the state being declared. 36 | 37 | With a state block you may declare events, entry actions, and exit actions. The event method is used to declare transition out of the current state. Its parameters are the event, destination state, and an optional action. The on_entry and on_exit methods are straight forward. They take one parameter: an action. (See below for more on action syntax) 38 | 39 | After the waiting state declaration we see the familiar calls to trans. The trans method takes an option 4th action parameter. You can see that the release and refund actions were added this way. 40 | Context: 41 | 42 | The final line sets the context of the statemachine. This is an interesting aspect. Every statemachine may have a context and if your statemachine has actions, you should definitely give it a context. Every action of a statemachine will be executed within its context object. We’ll discuss this more later. 43 | 44 | Here is a simple context for the vending machine statemachine. 45 | 46 |
class VendingMachineContext
47 | 
48 |   def activate
49 |     puts "activating"
50 |   end
51 | 
52 |   def release(product)
53 |     puts "releasing product: #{product}"
54 |   end
55 | 
56 |   def refund
57 |     puts "refuding dollar"
58 |   end
59 | 
60 |   def sales_mode
61 |     puts "going into sales mode"
62 |   end
63 | 
64 |   def operation_mode
65 |     puts "going into operation mode"
66 |   end
67 | 
68 | end
69 | 70 |

Action Declarations:

71 | 72 | With the statemachine gem, actions can be declared in any of three forms: Symbol, String, or Block. 73 | 74 | When the action is a Symbol, (on_entry :sales_mode) it is assumes that there is a method by the same name on the context class. This method will be invoked. Any parameters in with the event will be passed along to the invoked method. 75 | 76 | String actions should contains ruby code (on_entry "puts 'entering sales mode'"). The string will use invoked with in the context object using instance_eval. Strings allow quick and dirty actions without the overhead of defining methods on your context class. The disadvantage of String actions is that they cannot accept parameters. 77 | 78 | If the action is a Proc (on_entry Proc.new {puts 'entering sales mode'}), it will be called within the context of the context. Proc actions are also nice for quick and dirty actions. They can accept parameters and are preferred to String actions, unless you want to marshal your statemachine. Using one Proc actions will prevent the entire statemachine from being marhsal-able. 79 | 80 |

Execution

81 | 82 | For kicks let’s put this statemachine thought a few events. 83 | 84 |
vending_machine.dollar
85 | vending_machine.dollar
86 | vending_machine.selection "Peanuts"
87 | 88 | Here’s the output: 89 | 90 |
going into operation mode
91 | activating
92 | refuding dollar
93 | releasing product: Peanuts
94 | going into sales mode
95 | 96 | That sums it up for actions. In Example 3, we’ll talk about how do deal with conditional logic in your statemachine. -------------------------------------------------------------------------------- /rails_plugin/spec/models/vending_machine_interface_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe "Vending Machine Interface" do 4 | 5 | before(:each) do 6 | @display = VendingMachineInterface.new 7 | @vm = VendingMachine.new 8 | @water = @vm.add_product(10, "Water", 100) 9 | @tea = @vm.add_product(10, "Tea", 125) 10 | @chocolate = @vm.add_product(10, "Chocolate", 135) 11 | @danish = @vm.add_product(10, "Danish", 140) 12 | @display.vending_machine = @vm 13 | end 14 | 15 | it "message" do 16 | @display.message.should eql("Insert Money") 17 | @display.add_dollar 18 | @display.message.should eql(")$1.00" 19 | @display.add_quarter 20 | @display.message.should eql(")$1.25" 21 | @display.add_dime 22 | @display.message.should eql(")$1.35" 23 | @display.add_nickel 24 | @display.message.should eql(")$1.40" 25 | end 26 | 27 | it "message when refusing money" do 28 | @display.add_dollar 29 | @display.refuse_money 30 | @display.message.should eql("Select Item") 31 | end 32 | 33 | it "available items" do 34 | @display.affordable_items.should eql [] 35 | @display.non_affordable_items.should eql [@water, @tea, @chocolate, @danish] 36 | 37 | @display.add_dollar 38 | @display.affordable_items.should eql [@water] 39 | @display.non_affordable_items.should eql [@tea, @chocolate, @danish] 40 | 41 | @display.add_quarter 42 | @display.affordable_items.should eql [@water, @tea] 43 | @display.non_affordable_items.should eql [@chocolate, @danish] 44 | 45 | @display.add_dime 46 | @display.affordable_items.should eql [@water, @tea, @chocolate] 47 | @display.non_affordable_items.should eql [@danish] 48 | 49 | @display.add_nickel 50 | @display.affordable_items.should eql [@water, @tea, @chocolate, @danish] 51 | @display.non_affordable_items.should eql [] 52 | end 53 | 54 | it "sold out items" do 55 | @display.sold_out_items.should eql [] 56 | @water.inventory = 0 57 | @display.sold_out_items.should eql [@water] 58 | @danish.inventory = 0 59 | @display.sold_out_items.should eql [@water, @danish] 60 | 61 | @display.add_dollar 62 | @display.add_quarter 63 | @display.affordable_items.should eql [@tea] 64 | @display.non_affordable_items.should eql [@chocolate] 65 | end 66 | 67 | it "dispense change" do 68 | @display.add_dollar 69 | @display.dispense_change 70 | 71 | @display.amount_tendered.should equal 0 72 | @display.change.should eql(")$1.00" 73 | end 74 | 75 | end 76 | 77 | 78 | describe "Vending Machine Display Spec" do 79 | 80 | before(:each) do 81 | @display = VendingMachineInterface.new 82 | @sm = mock("vending statemachine") 83 | @display.statemachine = @sm 84 | 85 | @vending_machine = VendingMachine.new 86 | @milk = @vending_machine.add_product(10, "Milk", 100) 87 | @whiskey = @vending_machine.add_product(10, "Whiskey", 200) 88 | @vending_machine.save! 89 | 90 | @display.vending_machine = @vending_machine 91 | end 92 | 93 | it "start state" do 94 | @display.amount_tendered.should equal 0 95 | @display.accepting_money.should equal true 96 | end 97 | 98 | it "dollar" do 99 | @display.add_dollar 100 | @display.amount_tendered.should equal 100 101 | @display.accepting_money.should equal true 102 | end 103 | 104 | it "quarter" do 105 | @display.add_quarter 106 | @display.amount_tendered.should equal 25 107 | @display.accepting_money.should equal true 108 | end 109 | 110 | it "dime" do 111 | @display.add_dime 112 | @display.amount_tendered.should equal 10 113 | @display.accepting_money.should equal true 114 | end 115 | 116 | it "nickel" do 117 | @display.add_nickel 118 | @display.amount_tendered.should equal 5 119 | @display.accepting_money.should equal true 120 | end 121 | 122 | it "dollar when max price is a dollar" do 123 | @display.refuse_money 124 | @display.accepting_money.should equal false 125 | end 126 | 127 | it "make sale" do 128 | @display.add_dollar 129 | @display.load_product(@milk.id) 130 | @display.make_sale 131 | 132 | @display.amount_tendered.should equal 0 133 | @display.dispensed_item.name.should eql("Milk") 134 | @display.change.should eql(")$0.00" 135 | Product.find(@milk.id).inventory.should equal 9 136 | @vending_machine.cash.should equal 100 137 | end 138 | 139 | it "check affordability with inssuficient funds" do 140 | @display.add_dollar 141 | @display.load_product(@whiskey.id) 142 | 143 | @sm.should_receive(:refuse_purchase) 144 | @display.check_affordability 145 | end 146 | 147 | it "check affordability with suficient funds" do 148 | @display.add_dollar 149 | @display.add_dollar 150 | @display.load_product(@whiskey.id) 151 | 152 | @sm.should_receive(:accept_purchase) 153 | @display.check_affordability 154 | end 155 | 156 | it "check affordability with inssuficient funds" do 157 | @whiskey.inventory = 0 158 | @whiskey.save! 159 | 160 | @display.add_dollar 161 | @display.add_dollar 162 | @display.load_product(@whiskey.id) 163 | 164 | @sm.should_receive(:refuse_purchase) 165 | @display.check_affordability 166 | end 167 | 168 | end 169 | -------------------------------------------------------------------------------- /lib/statemachine/generate/dot_graph/dot_graph_statemachine.rb: -------------------------------------------------------------------------------- 1 | require 'statemachine/generate/util' 2 | require 'statemachine/generate/src_builder' 3 | 4 | module Statemachine 5 | class Statemachine 6 | 7 | attr_reader :states 8 | 9 | def to_dot(options = {}) 10 | generator = Generate::DotGraph::DotGraphStatemachine.new(self, options) 11 | generator.generate! 12 | end 13 | 14 | end 15 | 16 | module Generate 17 | module DotGraph 18 | 19 | class DotGraphStatemachine 20 | 21 | include Generate::Util 22 | 23 | def initialize(sm, options) 24 | @sm = sm 25 | @output_dir = options[:output] 26 | raise "Please specify an output directory. (:output => 'where/you/want/your/code')" if @output_dir.nil? 27 | raise "Output dir '#{@output_dir}' doesn't exist." if !File.exist?(@output_dir) 28 | end 29 | 30 | def generate! 31 | explore_sm 32 | save_output(src_file("main"), build_full_graph) 33 | @sm.states.values.each do |state| 34 | save_output(src_file("#{state.id}"), build_state_graph(state)) 35 | end 36 | end 37 | 38 | private 39 | 40 | def explore_sm 41 | @nodes = [] 42 | @transitions = [] 43 | @sm.states.values.each { |state| 44 | state.transitions.values.each { |transition| 45 | @nodes << transition.origin_id 46 | if transition.destination_id.to_s =~ /_H$/ 47 | dest = transition.destination_id.to_s.sub(/_H$/, '').to_sym 48 | @nodes << dest 49 | @transitions << Transition.new(transition.origin_id, dest, transition.event.to_s + '_H', nil) 50 | else 51 | @nodes << transition.destination_id 52 | @transitions << transition 53 | end 54 | } 55 | if Superstate === state and state.startstate_id 56 | @nodes << state.startstate_id 57 | @transitions << Transition.new(state.id, state.startstate_id, :start, nil) 58 | end 59 | if state.default_transition 60 | transition = state.default_transition 61 | @transitions << Transition.new(transition.origin_id, transition.destination_id, '*', nil) 62 | end 63 | } 64 | @transitions = @transitions.uniq 65 | @nodes = @nodes.uniq 66 | end 67 | 68 | def build_full_graph 69 | builder = Generate::SrcBuilder.new 70 | 71 | add_graph_header(builder, "main") 72 | 73 | # Create graph tree 74 | @sm.states.values.each { |state| 75 | class << state 76 | attr_reader :children 77 | def add_child(child) 78 | (@children ||= []) << child 79 | end 80 | end 81 | } 82 | 83 | roots = [] 84 | @sm.states.values.each { |state| 85 | if state.superstate.id == :root 86 | roots << state 87 | else 88 | state.superstate.add_child(state) 89 | end 90 | } 91 | 92 | roots.each do |root| 93 | add_node(builder, root) 94 | end 95 | 96 | builder << endl 97 | 98 | @transitions.each do |transition| 99 | add_transition(builder, transition) 100 | end 101 | 102 | add_graph_footer(builder) 103 | 104 | return builder.to_s 105 | end 106 | 107 | def build_state_graph(state) 108 | builder = Generate::SrcBuilder.new 109 | 110 | add_graph_header(builder, state.id) 111 | 112 | state.transitions.values.each do |transition| 113 | add_transition(builder, transition) 114 | end 115 | 116 | add_graph_footer(builder) 117 | 118 | return builder.to_s 119 | end 120 | 121 | def add_graph_header(builder, graph_name) 122 | builder << "digraph #{graph_name} {" << endl 123 | builder << " compound=true; compress=true; rankdir=LR;" 124 | builder << endl 125 | builder.indent! 126 | end 127 | 128 | def add_graph_footer(builder) 129 | builder.undent! 130 | builder << "}" << endl 131 | end 132 | 133 | def add_node(builder, node) 134 | if Superstate === node 135 | builder << "subgraph cluster_#{node.id} { " 136 | builder.indent! 137 | builder << endl 138 | builder << "label = \"#{node.id}\"; style=rounded; #{node.id} " 139 | builder << " [ style=\"rounded,filled\", shape=box, href=\"#{node.id}.svg\" ];" 140 | builder << endl 141 | node.children.each do |child| 142 | add_node(builder, child) 143 | end 144 | builder.undent! 145 | builder << "}" 146 | builder << endl 147 | else 148 | builder << node.id 149 | builder << " [shape=box, style=rounded, href=\"#{node.id}.svg\" ]" 150 | builder << endl 151 | end 152 | end 153 | 154 | def add_transition(builder, transition) 155 | builder << transition.origin_id 156 | builder << " -> " 157 | builder << transition.destination_id 158 | builder << " [ " 159 | builder << "label = \"#{transition.event}\" " 160 | if transition.event.to_s =~ /_H$/ 161 | dest = 'cluster_' + transition.destination_id.to_s 162 | builder << ", lhead=#{dest}" 163 | end 164 | builder << "]" 165 | builder << endl 166 | end 167 | 168 | def src_file(name) 169 | return name if @output_dir.nil? 170 | path = @output_dir 171 | answer = File.join(path, "#{name}.dot") 172 | return answer 173 | end 174 | 175 | def save_output(filename, content) 176 | if @output_dir.nil? 177 | say "Writing to file: #{filename}" 178 | else 179 | create_file(filename, content) 180 | end 181 | end 182 | end 183 | end 184 | end 185 | end 186 | -------------------------------------------------------------------------------- /lib/statemachine/statemachine.rb: -------------------------------------------------------------------------------- 1 | module Statemachine 2 | 3 | class StatemachineException < StandardError 4 | end 5 | 6 | class TransitionMissingException < StandardError 7 | end 8 | 9 | # Used at runtime to execute the behavior of the statemachine. 10 | # Should be created by using the Statemachine.build method. 11 | # 12 | # sm = Statemachine.build do 13 | # trans :locked, :coin, :unlocked 14 | # trans :unlocked, :pass, :locked: 15 | # end 16 | # 17 | # sm.coin 18 | # sm.state 19 | # 20 | # This class will accept any method that corresponds to an event. If the 21 | # current state respons to the event, the appropriate transtion will be invoked. 22 | # Otherwise an exception will be raised. 23 | class Statemachine 24 | include ActionInvokation 25 | 26 | # The tracer is an IO object. The statemachine will write run time execution 27 | # information to the +tracer+. Can be helpful in debugging. Defaults to nil. 28 | attr_accessor :tracer 29 | 30 | # Provides access to the +context+ of the statemachine. The context is a object 31 | # where all actions will be invoked. This provides a way to separate logic from 32 | # behavior. The statemachine is responsible for all the logic and the context 33 | # is responsible for all the behavior. 34 | attr_accessor :context 35 | 36 | # A statemachine can be named. This is most useful when a program is running 37 | # multiple machines with a tracer. The name is prepended to the tracer notices 38 | # so that they can be matched to the correct statemachine. 39 | attr_accessor :name 40 | 41 | # This callback would be called whenever the statemachine is changing the state. 42 | # This is useful whenever we want to make the state persistent and store into 43 | # some database. 44 | attr_accessor :state_change_action 45 | 46 | attr_reader :root #:nodoc: 47 | 48 | # Should not be called directly. Instances of Statemachine::Statemachine are created 49 | # through the Statemachine.build method. 50 | def initialize(root = Superstate.new(:root, nil, self)) 51 | @root = root 52 | @states = {} 53 | @tracer = nil 54 | end 55 | 56 | # Returns the id of the startstate of the statemachine. 57 | def startstate 58 | return @root.startstate_id 59 | end 60 | 61 | # Resets the statemachine back to its starting state. 62 | def reset 63 | self.state = get_state(@root.startstate_id) 64 | while @state and not @state.concrete? 65 | self.state = get_state(@state.startstate_id) 66 | end 67 | raise StatemachineException.new("The state machine doesn't know where to start. Try setting the startstate.") if @state == nil 68 | @state.enter 69 | 70 | @states.values.each { |state| state.reset } 71 | end 72 | 73 | # Return the id of the current state of the statemachine. 74 | def state 75 | return @state.id 76 | end 77 | 78 | # You may change the state of the statemachine by using this method. The parameter should be 79 | # the id of the desired state. 80 | def state= value 81 | if value.is_a? State 82 | @state = value 83 | elsif @states[value] 84 | @state = @states[value] 85 | elsif value and @states[value.to_sym] 86 | @state = @states[value.to_sym] 87 | end 88 | 89 | invoke_action(state_change_action, [@state], "state change action to #{@state}") if state_change_action 90 | end 91 | 92 | # The key method to exercise the statemachine. Any extra arguments supplied will be passed into 93 | # any actions associated with the transition. 94 | # 95 | # Alternatively to this method, you may invoke methods, names the same as the event, on the statemachine. 96 | # The advantage of using +process_event+ is that errors messages are more informative. 97 | def process_event(event, *args) 98 | event = event.to_sym 99 | trace "Event: #{event}" 100 | if @state 101 | transition = @state.transition_for(event) 102 | if transition 103 | transition.invoke(@state, self, args) 104 | else 105 | raise TransitionMissingException.new("#{@state} does not respond to the '#{event}' event.") 106 | end 107 | else 108 | raise StatemachineException.new("The state machine isn't in any state while processing the '#{event}' event.") 109 | end 110 | end 111 | 112 | def trace(message) #:nodoc: 113 | if @tracer 114 | prefix = @name ? "(#{@name}) " : "" 115 | @tracer.puts("#{prefix}#{message}") 116 | end 117 | end 118 | 119 | def get_state(id) #:nodoc: 120 | if @states.has_key? id 121 | return @states[id] 122 | elsif(is_history_state_id?(id)) 123 | superstate_id = base_id(id) 124 | superstate = @states[superstate_id] 125 | raise StatemachineException.new("No history exists for #{superstate} since it is not a super state.") if superstate.concrete? 126 | return load_history(superstate) 127 | else 128 | state = State.new(id, @root, self) 129 | @states[id] = state 130 | return state 131 | end 132 | end 133 | 134 | def add_state(state) #:nodoc: 135 | @states[state.id] = state 136 | end 137 | 138 | def has_state(id) #:nodoc: 139 | if(is_history_state_id?(id)) 140 | return @states.has_key?(base_id(id)) 141 | else 142 | return @states.has_key?(id) 143 | end 144 | end 145 | 146 | def respond_to?(message) 147 | return true if super(message) 148 | return true if @state and @state.transition_for(message) 149 | return false 150 | end 151 | 152 | def method_missing(message, *args) #:nodoc: 153 | if @state and @state.transition_for(message) 154 | process_event(message.to_sym, *args) 155 | # method = self.method(:process_event) 156 | # params = [message.to_sym].concat(args) 157 | # method.call(*params) 158 | else 159 | begin 160 | super(message, args) 161 | rescue NoMethodError 162 | process_event(message.to_sym, *args) 163 | end 164 | end 165 | end 166 | 167 | private 168 | 169 | def is_history_state_id?(id) 170 | return id.to_s[-2..-1] == "_H" 171 | end 172 | 173 | def base_id(history_id) 174 | return history_id.to_s[0...-2].to_sym 175 | end 176 | 177 | def load_history(superstate) 178 | 100.times do 179 | history = superstate.history_id ? get_state(superstate.history_id) : nil 180 | raise StatemachineException.new("#{superstate} doesn't have any history yet.") if not history 181 | if history.concrete? 182 | return history 183 | else 184 | superstate = history 185 | end 186 | end 187 | raise StatemachineException.new("No history found within 100 levels of nested superstates.") 188 | end 189 | 190 | end 191 | end 192 | -------------------------------------------------------------------------------- /spec/builder_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/spec_helper") 2 | 3 | describe "Builder" do 4 | 5 | before(:each) do 6 | @log = [] 7 | end 8 | 9 | def check_switch(sm) 10 | sm.state.should equal(:off) 11 | 12 | sm.toggle 13 | @log[0].should eql("toggle on") 14 | sm.state.should equal(:on) 15 | 16 | sm.toggle 17 | @log[1].should eql("toggle off") 18 | sm.state.should equal(:off) 19 | end 20 | 21 | it "Building a the switch, relaxed" do 22 | sm = Statemachine.build do 23 | trans :off, :toggle, :on, Proc.new { @log << "toggle on" } 24 | trans :on, :toggle, :off, Proc.new { @log << "toggle off" } 25 | end 26 | sm.context = self 27 | 28 | check_switch sm 29 | end 30 | 31 | it "Building a the switch, strict" do 32 | sm = Statemachine.build do 33 | state(:off) { |s| s.event :toggle, :on, Proc.new { @log << "toggle on" } } 34 | state(:on) { |s| s.event :toggle, :off, Proc.new { @log << "toggle off" } } 35 | end 36 | sm.context = self 37 | 38 | check_switch sm 39 | end 40 | 41 | it "Adding a superstate to the switch" do 42 | the_context = self 43 | sm = Statemachine.build do 44 | superstate :operation do 45 | event :admin, :testing, lambda { @log << "testing" } 46 | trans :off, :toggle, :on, lambda { @log << "toggle on" } 47 | trans :on, :toggle, :off, lambda { @log << "toggle off" } 48 | startstate :on 49 | end 50 | trans :testing, :resume, :operation, lambda { @log << "resuming" } 51 | startstate :off 52 | context the_context 53 | end 54 | 55 | sm.state.should equal(:off) 56 | sm.toggle 57 | sm.admin 58 | sm.state.should equal(:testing) 59 | sm.resume 60 | sm.state.should equal(:on) 61 | @log.join(",").should eql("toggle on,testing,resuming") 62 | end 63 | 64 | it "entry exit actions" do 65 | the_context = self 66 | sm = Statemachine.build do 67 | state :off do 68 | on_entry Proc.new { @log << "enter off" } 69 | event :toggle, :on, lambda { @log << "toggle on" } 70 | on_exit Proc.new { @log << "exit off" } 71 | end 72 | trans :on, :toggle, :off, lambda { @log << "toggle off" } 73 | context the_context 74 | end 75 | 76 | sm.toggle 77 | sm.state.should equal(:on) 78 | sm.toggle 79 | sm.state.should equal(:off) 80 | 81 | @log.join(",").should eql("enter off,exit off,toggle on,toggle off,enter off") 82 | end 83 | 84 | it "History state" do 85 | the_context = self 86 | sm = Statemachine.build do 87 | superstate :operation do 88 | event :admin, :testing, lambda { @log << "testing" } 89 | state :off do |off| 90 | on_entry Proc.new { @log << "enter off" } 91 | event :toggle, :on, lambda { @log << "toggle on" } 92 | end 93 | trans :on, :toggle, :off, lambda { @log << "toggle off" } 94 | startstate :on 95 | end 96 | trans :testing, :resume, :operation_H, lambda { @log << "resuming" } 97 | startstate :off 98 | context the_context 99 | end 100 | 101 | sm.admin 102 | sm.resume 103 | sm.state.should equal(:off) 104 | 105 | @log.join(",").should eql("enter off,testing,resuming,enter off") 106 | end 107 | 108 | it "entry and exit action created from superstate builder" do 109 | the_context = self 110 | sm = Statemachine.build do 111 | trans :off, :toggle, :on, Proc.new { @log << "toggle on" } 112 | on_entry_of :off, Proc.new { @log << "entering off" } 113 | trans :on, :toggle, :off, Proc.new { @log << "toggle off" } 114 | on_exit_of :on, Proc.new { @log << "exiting on" } 115 | context the_context 116 | end 117 | 118 | sm.toggle 119 | sm.toggle 120 | 121 | @log.join(",").should eql("entering off,toggle on,exiting on,toggle off,entering off") 122 | 123 | end 124 | 125 | it "superstate as startstate" do 126 | 127 | lambda do 128 | sm = Statemachine.build do 129 | superstate :mario_bros do 130 | trans :luigi, :bother, :mario 131 | end 132 | end 133 | 134 | sm.state.should equal(:luigi) 135 | end.should_not raise_error(Exception) 136 | end 137 | 138 | it "setting the start state before any other states declared" do 139 | 140 | sm = Statemachine.build do 141 | startstate :right 142 | trans :left, :push, :middle 143 | trans :middle, :push, :right 144 | trans :right, :pull, :middle 145 | end 146 | 147 | sm.state.should equal(:right) 148 | sm.pull 149 | sm.state.should equal(:middle) 150 | end 151 | 152 | it "setting start state which is in a super state" do 153 | sm = Statemachine.build do 154 | startstate :right 155 | superstate :table do 156 | event :tilt, :floor 157 | trans :left, :push, :middle 158 | trans :middle, :push, :right 159 | trans :right, :pull, :middle 160 | end 161 | state :floor 162 | end 163 | 164 | sm.state.should equal(:right) 165 | sm.pull 166 | sm.state.should equal(:middle) 167 | sm.push 168 | sm.state.should equal(:right) 169 | sm.tilt 170 | sm.state.should equal(:floor) 171 | end 172 | 173 | it "can set context" do 174 | widget = Object.new 175 | sm = Statemachine.build do 176 | context widget 177 | end 178 | 179 | sm.context.should be(widget) 180 | end 181 | 182 | it "statemachine will be set on context if possible" do 183 | class Widget 184 | attr_accessor :statemachine 185 | end 186 | widget = Widget.new 187 | 188 | sm = Statemachine.build do 189 | context widget 190 | end 191 | 192 | sm.context.should be(widget) 193 | widget.statemachine.should be(sm) 194 | end 195 | 196 | it "should have an on_event" do 197 | sm = Statemachine.build do 198 | startstate :start 199 | state :start do 200 | on_event :go, :transition_to => :new_state 201 | end 202 | end 203 | sm.go 204 | sm.state.should == :new_state 205 | end 206 | 207 | it "should trigger actions using on_event" do 208 | sm = Statemachine.build do 209 | startstate :start 210 | state :start do 211 | on_event :go, :transition_to => :new_state, :and_perform => :action 212 | end 213 | end 214 | object = mock("context") 215 | sm.context = object 216 | object.should_receive(:action) 217 | 218 | sm.go 219 | end 220 | 221 | it "should have a transition_from" do 222 | sm = Statemachine.build do 223 | transition_from :start, :on_event => :go, :transition_to => :new_state 224 | end 225 | 226 | sm.go 227 | sm.state.should == :new_state 228 | end 229 | 230 | it "should trigger actions on transition_from" do 231 | sm = Statemachine.build do 232 | transition_from :start, :on_event => :go, :transition_to => :new_state, :and_perform => :action 233 | end 234 | object = mock("context") 235 | sm.context = object 236 | object.should_receive(:action) 237 | 238 | sm.go 239 | end 240 | end 241 | 242 | -------------------------------------------------------------------------------- /rails_plugin/README: -------------------------------------------------------------------------------- 1 | == Welcome to Rails 2 | 3 | Rails is a web-application and persistence framework that includes everything 4 | needed to create database-backed web-applications according to the 5 | Model-View-Control pattern of separation. This pattern splits the view (also 6 | called the presentation) into "dumb" templates that are primarily responsible 7 | for inserting pre-built data in between HTML tags. The model contains the 8 | "smart" domain objects (such as Account, Product, Person, Post) that holds all 9 | the business logic and knows how to persist themselves to a database. The 10 | controller handles the incoming requests (such as Save New Account, Update 11 | Product, Show Post) by manipulating the model and directing data to the view. 12 | 13 | In Rails, the model is handled by what's called an object-relational mapping 14 | layer entitled Active Record. This layer allows you to present the data from 15 | database rows as objects and embellish these data objects with business logic 16 | methods. You can read more about Active Record in 17 | link:files/vendor/rails/activerecord/README.html. 18 | 19 | The controller and view are handled by the Action Pack, which handles both 20 | layers by its two parts: Action View and Action Controller. These two layers 21 | are bundled in a single package due to their heavy interdependence. This is 22 | unlike the relationship between the Active Record and Action Pack that is much 23 | more separate. Each of these packages can be used independently outside of 24 | Rails. You can read more about Action Pack in 25 | link:files/vendor/rails/actionpack/README.html. 26 | 27 | 28 | == Getting started 29 | 30 | 1. Start the web server: ruby script/server (run with --help for options) 31 | 2. Go to http://localhost:3000/ and get "Welcome aboard: You’re riding the Rails!" 32 | 3. Follow the guidelines to start developing your application 33 | 34 | 35 | == Web servers 36 | 37 | Rails uses the built-in web server in Ruby called WEBrick by default, so you don't 38 | have to install or configure anything to play around. 39 | 40 | If you have lighttpd installed, though, it'll be used instead when running script/server. 41 | It's considerably faster than WEBrick and suited for production use, but requires additional 42 | installation and currently only works well on OS X/Unix (Windows users are encouraged 43 | to start with WEBrick). We recommend version 1.4.11 and higher. You can download it from 44 | http://www.lighttpd.net. 45 | 46 | If you want something that's halfway between WEBrick and lighttpd, we heartily recommend 47 | Mongrel. It's a Ruby-based web server with a C-component (so it requires compilation) that 48 | also works very well with Windows. See more at http://mongrel.rubyforge.org/. 49 | 50 | But of course its also possible to run Rails with the premiere open source web server Apache. 51 | To get decent performance, though, you'll need to install FastCGI. For Apache 1.3, you want 52 | to use mod_fastcgi. For Apache 2.0+, you want to use mod_fcgid. 53 | 54 | See http://wiki.rubyonrails.com/rails/pages/FastCGI for more information on FastCGI. 55 | 56 | == Example for Apache conf 57 | 58 | 59 | ServerName rails 60 | DocumentRoot /path/application/public/ 61 | ErrorLog /path/application/log/server.log 62 | 63 | 64 | Options ExecCGI FollowSymLinks 65 | AllowOverride all 66 | Allow from all 67 | Order allow,deny 68 | 69 | 70 | 71 | NOTE: Be sure that CGIs can be executed in that directory as well. So ExecCGI 72 | should be on and ".cgi" should respond. All requests from 127.0.0.1 go 73 | through CGI, so no Apache restart is necessary for changes. All other requests 74 | go through FCGI (or mod_ruby), which requires a restart to show changes. 75 | 76 | 77 | == Debugging Rails 78 | 79 | Have "tail -f" commands running on both the server.log, production.log, and 80 | test.log files. Rails will automatically display debugging and runtime 81 | information to these files. Debugging info will also be shown in the browser 82 | on requests from 127.0.0.1. 83 | 84 | 85 | == Breakpoints 86 | 87 | Breakpoint support is available through the script/breakpointer client. This 88 | means that you can break out of execution at any point in the code, investigate 89 | and change the model, AND then resume execution! Example: 90 | 91 | class WeblogController < ActionController::Base 92 | def index 93 | @posts = Post.find_all 94 | breakpoint "Breaking out from the list" 95 | end 96 | end 97 | 98 | So the controller will accept the action, run the first line, then present you 99 | with a IRB prompt in the breakpointer window. Here you can do things like: 100 | 101 | Executing breakpoint "Breaking out from the list" at .../webrick_server.rb:16 in 'breakpoint' 102 | 103 | >> @posts.inspect 104 | => "[#nil, \"body\"=>nil, \"id\"=>\"1\"}>, 105 | #\"Rails you know!\", \"body\"=>\"Only ten..\", \"id\"=>\"2\"}>]" 106 | >> @posts.first.title = "hello from a breakpoint" 107 | => "hello from a breakpoint" 108 | 109 | ...and even better is that you can examine how your runtime objects actually work: 110 | 111 | >> f = @posts.first 112 | => #nil, "body"=>nil, "id"=>"1"}> 113 | >> f. 114 | Display all 152 possibilities? (y or n) 115 | 116 | Finally, when you're ready to resume execution, you press CTRL-D 117 | 118 | 119 | == Console 120 | 121 | You can interact with the domain model by starting the console through script/console. 122 | Here you'll have all parts of the application configured, just like it is when the 123 | application is running. You can inspect domain models, change values, and save to the 124 | database. Starting the script without arguments will launch it in the development environment. 125 | Passing an argument will specify a different environment, like script/console production. 126 | 127 | To reload your controllers and models after launching the console run reload! 128 | 129 | 130 | 131 | == Description of contents 132 | 133 | app 134 | Holds all the code that's specific to this particular application. 135 | 136 | app/controllers 137 | Holds controllers that should be named like weblog_controller.rb for 138 | automated URL mapping. All controllers should descend from 139 | ActionController::Base. 140 | 141 | app/models 142 | Holds models that should be named like post.rb. 143 | Most models will descend from ActiveRecord::Base. 144 | 145 | app/views 146 | Holds the template files for the view that should be named like 147 | weblog/index.rhtml for the WeblogController#index action. All views use eRuby 148 | syntax. This directory can also be used to keep stylesheets, images, and so on 149 | that can be symlinked to public. 150 | 151 | app/helpers 152 | Holds view helpers that should be named like weblog_helper.rb. 153 | 154 | app/apis 155 | Holds API classes for web services. 156 | 157 | config 158 | Configuration files for the Rails environment, the routing map, the database, and other dependencies. 159 | 160 | components 161 | Self-contained mini-applications that can bundle together controllers, models, and views. 162 | 163 | db 164 | Contains the database schema in schema.rb. db/migrate contains all 165 | the sequence of Migrations for your schema. 166 | 167 | lib 168 | Application specific libraries. Basically, any kind of custom code that doesn't 169 | belong under controllers, models, or helpers. This directory is in the load path. 170 | 171 | public 172 | The directory available for the web server. Contains subdirectories for images, stylesheets, 173 | and javascripts. Also contains the dispatchers and the default HTML files. 174 | 175 | script 176 | Helper scripts for automation and generation. 177 | 178 | test 179 | Unit and functional tests along with fixtures. 180 | 181 | vendor 182 | External libraries that the application depends on. Also includes the plugins subdirectory. 183 | This directory is in the load path. 184 | -------------------------------------------------------------------------------- /rails_plugin/public/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | Ruby on Rails: Welcome aboard 7 | 181 | 182 | 183 | 204 | 205 | 206 |
207 | 239 | 240 |
241 | 245 | 246 | 250 | 251 |
252 |

Getting started

253 |

Here’s how to get rolling:

254 | 255 |
    256 |
  1. 257 |

    Create your databases and edit config/database.yml

    258 |

    Rails needs to know your login and password.

    259 |
  2. 260 | 261 |
  3. 262 |

    Use script/generate to create your models and controllers

    263 |

    To see all available options, run it without parameters.

    264 |
  4. 265 | 266 |
  5. 267 |

    Set up a default route and remove or rename this file

    268 |

    Routes are setup in config/routes.rb.

    269 |
  6. 270 |
271 |
272 |
273 | 274 | 275 |
276 | 277 | -------------------------------------------------------------------------------- /lib/statemachine/builder.rb: -------------------------------------------------------------------------------- 1 | module Statemachine 2 | 3 | # The starting point for building instances of Statemachine. 4 | # The block passed in should contain all the declarations for all 5 | # states, events, and actions with in the statemachine. 6 | # 7 | # Sample: Turnstyle 8 | # 9 | # sm = Statemachine.build do 10 | # trans :locked, :coin, :unlocked, :unlock 11 | # trans :unlocked, :pass, :locked, :lock 12 | # end 13 | # 14 | # An optional statemachine parameter may be passed in to modify 15 | # an existing statemachine instance. 16 | # 17 | # Actions: 18 | # Where ever an action paramter is used, it may take on one of three forms: 19 | # 1. Symbols: will execute a method by the same name on the _context_ 20 | # 2. String: Ruby code that will be executed within the binding of the _context_ 21 | # 3. Proc: Will be executed within the binding of the _context_ 22 | # 23 | # See Statemachine::SuperstateBuilding 24 | # See Statemachine::StateBuilding 25 | # 26 | def self.build(statemachine = nil, &block) 27 | builder = statemachine ? StatemachineBuilder.new(statemachine) : StatemachineBuilder.new 28 | builder.instance_eval(&block) 29 | builder.statemachine.reset 30 | return builder.statemachine 31 | end 32 | 33 | class Builder #:nodoc: 34 | attr_reader :statemachine 35 | 36 | def initialize(statemachine) 37 | @statemachine = statemachine 38 | end 39 | 40 | protected 41 | def acquire_state_in(state_id, context) 42 | return nil if state_id == nil 43 | return state_id if state_id.is_a? State 44 | state = nil 45 | if @statemachine.has_state(state_id) 46 | state = @statemachine.get_state(state_id) 47 | else 48 | state = State.new(state_id, context, @statemachine) 49 | @statemachine.add_state(state) 50 | end 51 | context.startstate_id = state_id if context.startstate_id == nil 52 | return state 53 | end 54 | end 55 | 56 | # The builder module used to declare states. 57 | module StateBuilding 58 | attr_reader :subject 59 | 60 | # Declares that the state responds to the specified event. 61 | # The +event+ parameter should be a Symbol. 62 | # The +destination_id+, which should also be a Symbol, is the id of the state 63 | # that will event will transition into. 64 | # 65 | # The 3rd +action+ parameter is optional. Note that the action runs *before* 66 | # the transition to the state specified in the 2nd parameter. 67 | # 68 | # sm = Statemachine.build do 69 | # state :locked do 70 | # event :coin, :unlocked, :unlock 71 | # end 72 | # end 73 | # 74 | def event(event, destination_id, action = nil) 75 | @subject.add(Transition.new(@subject.id, destination_id, event, action)) 76 | end 77 | 78 | def on_event(event, options) 79 | self.event(event, options[:transition_to], options[:and_perform]) 80 | end 81 | 82 | # Declare the entry action for the state. 83 | # 84 | # sm = Statemachine.build do 85 | # state :locked do 86 | # on_entry :lock 87 | # end 88 | # end 89 | # 90 | def on_entry(entry_action) 91 | @subject.entry_action = entry_action 92 | end 93 | 94 | # Declare the exit action for the state. 95 | # 96 | # sm = Statemachine.build do 97 | # state :locked do 98 | # on_exit :unlock 99 | # end 100 | # end 101 | # 102 | def on_exit(exit_action) 103 | @subject.exit_action = exit_action 104 | end 105 | 106 | # Declare a default transition for the state. Any event that is not already handled 107 | # by the state will be handled by this transition. 108 | # 109 | # sm = Statemachine.build do 110 | # state :locked do 111 | # default :unlock, :action 112 | # end 113 | # end 114 | # 115 | def default(destination_id, action = nil) 116 | @subject.default_transition = Transition.new(@subject.id, destination_id, nil, action) 117 | end 118 | end 119 | 120 | # The builder module used to declare superstates. 121 | module SuperstateBuilding 122 | attr_reader :subject 123 | 124 | # Define a state within the statemachine or superstate. 125 | # 126 | # sm = Statemachine.build do 127 | # state :locked do 128 | # #define the state 129 | # end 130 | # end 131 | # 132 | def state(id, &block) 133 | builder = StateBuilder.new(id, @subject, @statemachine) 134 | builder.instance_eval(&block) if block 135 | end 136 | 137 | # Define a superstate within the statemachine or superstate. 138 | # 139 | # sm = Statemachine.build do 140 | # superstate :operational do 141 | # #define superstate 142 | # end 143 | # end 144 | # 145 | def superstate(id, &block) 146 | builder = SuperstateBuilder.new(id, @subject, @statemachine) 147 | builder.instance_eval(&block) 148 | end 149 | 150 | # Declares a transition within the superstate or statemachine. 151 | # The +origin_id+, a Symbol, identifies the starting state for this transition. The state 152 | # identified by +origin_id+ will be created within the statemachine or superstate which this 153 | # transition is declared. 154 | # The +event+ paramter should be a Symbol. 155 | # The +destination_id+, which should also be a Symbol, is the id of the state that will 156 | # event will transition into. This method will not create destination states within the 157 | # current statemachine of superstate. If the state destination state should exist here, 158 | # that declare with with the +state+ method or declare a transition starting at the state. 159 | # 160 | # sm = Statemachine.build do 161 | # trans :locked, :coin, :unlocked, :unlock 162 | # end 163 | # 164 | def trans(origin_id, event, destination_id, action = nil) 165 | origin = acquire_state_in(origin_id, @subject) 166 | origin.add(Transition.new(origin_id, destination_id, event, action)) 167 | end 168 | 169 | def transition_from(origin_id, options) 170 | trans(origin_id, options[:on_event], options[:transition_to], options[:and_perform]) 171 | end 172 | 173 | # Specifies the startstate for the statemachine or superstate. The state must 174 | # exist within the scope. 175 | # 176 | # sm = Statemachine.build do 177 | # startstate :locked 178 | # end 179 | # 180 | def startstate(startstate_id) 181 | @subject.startstate_id = startstate_id 182 | end 183 | 184 | # Allows the declaration of entry actions without using the +state+ method. +id+ is identifies 185 | # the state to which the entry action will be added. 186 | # 187 | # sm = Statemachine.build do 188 | # trans :locked, :coin, :unlocked 189 | # on_entry_of :unlocked, :unlock 190 | # end 191 | # 192 | def on_entry_of(id, action) 193 | @statemachine.get_state(id).entry_action = action 194 | end 195 | 196 | # Allows the declaration of exit actions without using the +state+ method. +id+ is identifies 197 | # the state to which the exit action will be added. 198 | # 199 | # sm = Statemachine.build do 200 | # trans :locked, :coin, :unlocked 201 | # on_exit_of :locked, :unlock 202 | # end 203 | # 204 | def on_exit_of(id, action) 205 | @statemachine.get_state(id).exit_action = action 206 | end 207 | 208 | # Used to specify the default state held by the history pseudo state of the superstate. 209 | # 210 | # sm = Statemachine.build do 211 | # superstate :operational do 212 | # default_history :state_id 213 | # end 214 | # end 215 | # 216 | def default_history(id) 217 | @subject.default_history = id 218 | end 219 | end 220 | 221 | # Builder class used to define states. Creates by SuperstateBuilding#state 222 | class StateBuilder < Builder 223 | include StateBuilding 224 | 225 | def initialize(id, superstate, statemachine) 226 | super statemachine 227 | @subject = acquire_state_in(id, superstate) 228 | end 229 | end 230 | 231 | # Builder class used to define superstates. Creates by SuperstateBuilding#superstate 232 | class SuperstateBuilder < Builder 233 | include StateBuilding 234 | include SuperstateBuilding 235 | 236 | def initialize(id, superstate, statemachine) 237 | super statemachine 238 | @subject = Superstate.new(id, superstate, statemachine) 239 | superstate.startstate_id = id if superstate.startstate_id == nil 240 | statemachine.add_state(@subject) 241 | end 242 | end 243 | 244 | # Created by Statemachine.build as the root context for building the statemachine. 245 | class StatemachineBuilder < Builder 246 | include SuperstateBuilding 247 | 248 | def initialize(statemachine = Statemachine.new) 249 | super statemachine 250 | @subject = @statemachine.root 251 | end 252 | 253 | # Used the set the context of the statemahine within the builder. 254 | # 255 | # sm = Statemachine.build do 256 | # ... 257 | # context MyContext.new 258 | # end 259 | # 260 | # Statemachine.context may also be used. 261 | def context(a_context) 262 | @statemachine.context = a_context 263 | a_context.statemachine = @statemachine if a_context.respond_to?(:statemachine=) 264 | end 265 | 266 | # Stubs the context. This makes statemachine immediately useable, even if functionless. 267 | # The stub will print all the actions called so it's nice for trial runs. 268 | # 269 | # sm = Statemachine.build do 270 | # ... 271 | # stub_context :verbose => true 272 | # end 273 | # 274 | # Statemachine.context may also be used. 275 | def stub_context(options={}) 276 | require 'statemachine/stub_context' 277 | context StubContext.new(options) 278 | end 279 | 280 | # This callback would be called whenever the statemachine is changing the state. 281 | # This is useful whenever we want to make the state persistent and store into 282 | # some database. 283 | def on_state_change(action=nil, &block) 284 | @statemachine.state_change_action = action || block 285 | end 286 | end 287 | 288 | end 289 | -------------------------------------------------------------------------------- /lib/statemachine/generate/java/java_statemachine.rb: -------------------------------------------------------------------------------- 1 | require 'statemachine/generate/util' 2 | require 'statemachine/generate/src_builder' 3 | 4 | module Statemachine 5 | class Statemachine 6 | 7 | attr_reader :states 8 | 9 | def to_java(options = {}) 10 | generator = Generate::Java::JavaStatemachine.new(self, options) 11 | generator.generate! 12 | end 13 | 14 | end 15 | 16 | module Generate 17 | module Java 18 | class JavaStatemachine 19 | 20 | include Generate::Util 21 | 22 | HEADER1 = "// This file was generated by the Ruby Statemachine Library (http://slagyr.github.com/statemachine)." 23 | HEADER2 = "// Generated at " 24 | 25 | def initialize(sm, options) 26 | @sm = sm 27 | @output_dir = options[:output] 28 | @classname = options[:name] 29 | @context_classname = "#{@classname}Context" 30 | @package = options[:package] 31 | raise "Please specify an output directory. (:output => 'where/you/want/your/code')" if @output_dir.nil? 32 | raise "Output dir '#{@output_dir}' doesn't exist." if !File.exist?(@output_dir) 33 | raise "Please specify a name for the statemachine. (:name => 'SomeName')" if @classname.nil? 34 | end 35 | 36 | def generate! 37 | explore_sm 38 | create_file(src_file(@classname), build_statemachine_src) 39 | create_file(src_file(@context_classname), build_context_src) 40 | say "Statemachine generated." 41 | end 42 | 43 | private ########################################### 44 | 45 | def explore_sm 46 | events = [] 47 | actions = [] 48 | @sm.states.values.each do |state| 49 | state.transitions.values.each do |transition| 50 | events << transition.event 51 | add_action(actions, transition.action) 52 | end 53 | end 54 | @event_names = events.uniq.map {|e| e.to_s.camalized(:lower)}.sort 55 | 56 | @sm.states.values.each do |state| 57 | add_action(actions, state.entry_action) 58 | add_action(actions, state.exit_action) 59 | end 60 | @action_names = actions.uniq.map {|e| e.to_s.camalized(:lower)}.sort 61 | 62 | @startstate = @sm.get_state(@sm.startstate).resolve_startstate 63 | end 64 | 65 | def add_action(actions, action) 66 | return if action.nil? 67 | raise "Actions must be symbols in order to generation Java code. (#{action})" unless action.is_a?(Symbol) 68 | actions << action 69 | end 70 | 71 | def build_statemachine_src 72 | src = begin_src 73 | src << "public class #{@classname}" << endl 74 | begin_scope(src) 75 | 76 | add_instance_variables(src) 77 | add_constructor(src) 78 | add_statemachine_boilerplate_code(src) 79 | add_event_delegation(src) 80 | add_statemachine_exception(src) 81 | add_base_state(src) 82 | add_state_implementations(src) 83 | 84 | end_scope(src) 85 | return src.to_s 86 | end 87 | 88 | def add_instance_variables (src) 89 | src << "// Instance variables" << endl 90 | concrete_states = @sm.states.values.reject { |state| state.id.nil? || !state.concrete? }.sort { |a, b| a.id <=> b.id } 91 | concrete_states.each do |state| 92 | name = state.id.to_s 93 | src << "public final State #{name.upcase} = new #{name.camalized}State(this);" << endl 94 | end 95 | superstates = @sm.states.values.reject { |state| state.concrete? }.sort { |a, b| a.id <=> b.id } 96 | superstates.each do |superstate| 97 | startstate = superstate.resolve_startstate 98 | src << "public final State #{superstate.id.to_s.upcase} = #{startstate.id.to_s.upcase};" << endl 99 | end 100 | src << "private State state = #{@startstate.id.to_s.upcase};" << endl 101 | src << endl 102 | src << "private #{@context_classname} context;" << endl 103 | src << endl 104 | end 105 | 106 | def add_constructor(src) 107 | src << "// Statemachine constructor" << endl 108 | add_method(src, nil, @classname, "#{@context_classname} context") do 109 | src << "this.context = context;" << endl 110 | entered_states = [] 111 | entry_state = @startstate 112 | while entry_state != @sm.root 113 | entered_states << entry_state 114 | entry_state = entry_state.superstate 115 | end 116 | entered_states.reverse.each do |state| 117 | src << "context.#{state.entry_action.to_s.camalized(:lower)}();" << endl if state.entry_action 118 | end 119 | end 120 | end 121 | 122 | def add_statemachine_boilerplate_code(src) 123 | src << "// The following is boiler plate code standard to all statemachines" << endl 124 | add_one_liner(src, @context_classname, "getContext", nil, "return context") 125 | add_one_liner(src, "State", "getState", nil, "return state") 126 | add_one_liner(src, "void", "setState", "State newState", "state = newState") 127 | end 128 | 129 | def add_event_delegation(src) 130 | src << "// Event delegation" << endl 131 | @event_names.each do |event| 132 | add_one_liner(src, "void", event, nil, "state.#{event}()") 133 | end 134 | end 135 | 136 | def add_statemachine_exception(src) 137 | src << "// Standard exception class added to all statemachines." << endl 138 | src << "public static class StatemachineException extends RuntimeException" << endl 139 | begin_scope(src) 140 | src << "public StatemachineException(State state, String event)" << endl 141 | begin_scope(src) 142 | src << "super(\"Missing transition from '\" + state.getClass().getSimpleName() + \"' with the '\" + event + \"' event.\");" << endl 143 | end_scope(src) 144 | end_scope(src) 145 | src << endl 146 | end 147 | 148 | def add_base_state(src) 149 | src << "// The base state" << endl 150 | src << "public static abstract class State" << endl 151 | begin_scope(src) 152 | src << "protected #{@classname} statemachine;" << endl 153 | src << endl 154 | add_one_liner(src, nil, "State", "#{@classname} statemachine", "this.statemachine = statemachine") 155 | @event_names.each do |event| 156 | add_one_liner(src, "void", event, nil, "throw new StatemachineException(this, \"#{event}\")") 157 | end 158 | end_scope(src) 159 | src << endl 160 | end 161 | 162 | def add_state_implementations(src) 163 | src << "// State implementations" << endl 164 | @sm.states.keys.reject{|k| k.nil? }.sort.each do |state_id| 165 | state = @sm.states[state_id] 166 | state_name = state.id.to_s.camalized 167 | base_class = state.superstate == @sm.root ? "State" : state.superstate.id.to_s.camalized 168 | 169 | add_concrete_state_class(src, state, state_name, base_class) if state_id 170 | end 171 | end 172 | 173 | def add_concrete_state_class(src, state, state_name, base_class) 174 | src << "public static class #{state_name}State extends State" << endl 175 | src << "{" << endl 176 | src.indent! 177 | add_one_liner(src, nil, "#{state_name}State", "#{@classname} statemachine", "super(statemachine)") 178 | state.transitions.keys.sort.each do |event_id| 179 | transition = state.transitions[event_id] 180 | add_state_event_handler(transition, src) 181 | end 182 | src.undent! 183 | src << "}" << endl 184 | src << endl 185 | end 186 | 187 | def add_state_event_handler(transition, src) 188 | event_name = transition.event.to_s.camalized(:lower) 189 | exits, entries = transition.exits_and_entries(@sm.get_state(transition.origin_id), @sm.get_state(transition.destination_id)) 190 | add_method(src, "void", event_name, nil) do 191 | exits.each do |exit| 192 | src << "statemachine.getContext().#{exit.exit_action.to_s.camalized(:lower)}();" << endl if exit.exit_action 193 | end 194 | src << "statemachine.getContext().#{transition.action.to_s.camalized(:lower)}();" << endl if transition.action 195 | src << "statemachine.setState(statemachine.#{transition.destination_id.to_s.upcase});" << endl 196 | entries.each do |entry| 197 | src << "statemachine.getContext().#{entry.entry_action.to_s.camalized(:lower)}();" << endl if entry.entry_action 198 | end 199 | end 200 | end 201 | 202 | def add_one_liner(src, return_type, name, params, body) 203 | add_method(src, return_type, name, params) do 204 | src << "#{body};" << endl 205 | end 206 | end 207 | 208 | def add_method(src, return_type, name, params) 209 | src << "public #{return_type} #{name}(#{params})".sub(' ' * 2, ' ') << endl 210 | begin_scope(src) 211 | yield 212 | end_scope(src) 213 | src << endl 214 | end 215 | 216 | def begin_scope(src) 217 | src << "{" << endl 218 | src.indent! 219 | end 220 | 221 | def end_scope(src) 222 | src.undent! << "}" << endl 223 | end 224 | 225 | def build_context_src 226 | src = begin_src 227 | src << "public interface #{@context_classname}" << endl 228 | begin_scope(src) 229 | src << "// Actions" << endl 230 | @action_names.each do |event| 231 | src << "void #{event}();" << endl 232 | end 233 | end_scope(src) 234 | return src.to_s 235 | end 236 | 237 | def begin_src 238 | src = SrcBuilder.new 239 | src << HEADER1 << endl 240 | src << HEADER2 << timestamp << endl 241 | src << "package #{@package};" << endl 242 | src << endl 243 | return src 244 | end 245 | 246 | def create_file(filename, content) 247 | establish_directory(File.dirname(filename)) 248 | say "Writing to file: #{filename}" 249 | File.open(filename, 'w') do |file| 250 | file.write(content) 251 | end 252 | end 253 | 254 | def src_file(name) 255 | path = @output_dir 256 | if @package 257 | @package.split(".").each { |segment| path = File.join(path, segment) } 258 | end 259 | return File.join(path, "#{name}.java") 260 | end 261 | 262 | end 263 | end 264 | end 265 | end 266 | --------------------------------------------------------------------------------