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 |
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.
"
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 |
--------------------------------------------------------------------------------
/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 |
Origin State
Event
Destination State
27 |
Waiting
dollar
Paid
28 |
Paid
selection
Waiting
29 |
30 |
Waiting
selection
Waiting
31 |
Paid
dollar
Paid
32 |
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 |
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 |
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 |