state_machine 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc CHANGED
@@ -1,5 +1,15 @@
1
1
  == master
2
2
 
3
+ == 0.2.1 / 2008-07-05
4
+
5
+ * Add more descriptive exceptions
6
+ * Assume the default state attribute is "state" if one is not provided
7
+ * Add :except_from option for transitions if you want to blacklist states
8
+ * Add PluginAWeek::StateMachine::Machine#states
9
+ * Add PluginAWeek::StateMachine::Event#transitions
10
+ * Allow creating transitions with no from state (effectively allowing the transition for *any* from state)
11
+ * Reduce the number of objects created for each transition
12
+
3
13
  == 0.2.0 / 2008-06-29
4
14
 
5
15
  * Add a non-bang version of events (e.g. park) that will return a boolean value for success
data/README.rdoc CHANGED
@@ -1,6 +1,7 @@
1
1
  == state_machine
2
2
 
3
- +state_machine+ adds support for creating state machines for attributes within a model.
3
+ +state_machine+ adds support for creating state machines for attributes within
4
+ a model.
4
5
 
5
6
  == Resources
6
7
 
@@ -28,15 +29,21 @@ and deciding how to behave based on the values in those columns. This can becom
28
29
  cumbersome and difficult to maintain when the complexity of your models starts to
29
30
  increase.
30
31
 
31
- +state_machine+ simplifies this design by introducing the various parts of a state
32
- machine, including states, events, and transitions. However, its api is designed
33
- to be similar to ActiveRecord in terms of validations and callbacks, making it
34
- so simple you don't even need to know what a state machine is :)
32
+ +state_machine+ simplifies this design by introducing the various parts of a real
33
+ state machine, including states, events, and transitions. However, the api is
34
+ designed to be similar to ActiveRecord in terms of validations and callbacks,
35
+ making it so simple you don't even need to know what a state machine is :)
35
36
 
36
37
  == Usage
37
38
 
38
39
  === Example
39
40
 
41
+ Below is an example of many of the features offered by this plugin, including
42
+ * Initial states
43
+ * State callbacks
44
+ * Event callbacks
45
+ * Conditional transitions
46
+
40
47
  class Vehicle < ActiveRecord::Base
41
48
  state_machine :state, :initial => 'idling' do
42
49
  before_exit 'parked', :put_on_seatbelt
@@ -74,8 +81,32 @@ so simple you don't even need to know what a state machine is :)
74
81
  transition :to => 'parked', :from => 'stalled', :if => :auto_shop_busy?
75
82
  end
76
83
  end
84
+
85
+ def tow!
86
+ end
87
+
88
+ def fix!
89
+ end
90
+
91
+ def auto_shop_busy?
92
+ false
93
+ end
77
94
  end
78
95
 
96
+ Using the above model as an example, you can interact with the state machine
97
+ like so:
98
+
99
+ vehicle = Vehicle.create # => #<Vehicle id: 1, seatbelt_on: false, state: "parked">
100
+ vehicle.ignite # => true
101
+ vehicle # => #<Vehicle id: 1, seatbelt_on: true, state: "idling">
102
+ vehicle.shift_up # => true
103
+ vehicle # => #<Vehicle id: 1, seatbelt_on: true, state: "first_gear">
104
+ vehicle.shift_up # => true
105
+ vehicle # => #<Vehicle id: 1, seatbelt_on: true, state: "second_gear">
106
+
107
+ # The bang (!) operator can raise exceptions if the event fails
108
+ vehicle.park! # => PluginAWeek::StateMachine::InvalidTransition: Cannot transition via :park from "second_gear"
109
+
79
110
  == Tools
80
111
 
81
112
  Jean Bovet - {Visual Automata Simulator}[http://www.cs.usfca.edu/~jbovet/vas.html].
data/Rakefile CHANGED
@@ -5,7 +5,7 @@ require 'rake/contrib/sshpublisher'
5
5
 
6
6
  spec = Gem::Specification.new do |s|
7
7
  s.name = 'state_machine'
8
- s.version = '0.2.0'
8
+ s.version = '0.2.1'
9
9
  s.platform = Gem::Platform::RUBY
10
10
  s.summary = 'Adds support for creating state machines for attributes within a model'
11
11
 
data/lib/state_machine.rb CHANGED
@@ -12,29 +12,65 @@ module PluginAWeek #:nodoc:
12
12
  end
13
13
 
14
14
  module MacroMethods
15
- # Creates a state machine for the given attribute.
15
+ # Creates a state machine for the given attribute. The default attribute
16
+ # is "state".
16
17
  #
17
18
  # Configuration options:
18
19
  # * +initial+ - The initial value of the attribute. This can either be the actual value or a Proc for dynamic initial states.
19
20
  #
20
- # == Example
21
+ # This also requires a block which will be used to actually configure the
22
+ # events and transitions for the state machine. *Note* that this block will
23
+ # be executed within the context of the state machine. As a result, you will
24
+ # not be able to access any class methods on the model unless you refer to
25
+ # them directly (i.e. specifying the class name).
21
26
  #
22
- # With a static state:
27
+ # For examples on the types of configured state machines and blocks, see
28
+ # the section below.
29
+ #
30
+ # == Examples
31
+ #
32
+ # With the default attribute and no initial state:
33
+ #
34
+ # class Switch < ActiveRecord::Base
35
+ # state_machine do
36
+ # event :park do
37
+ # ...
38
+ # end
39
+ # end
40
+ # end
41
+ #
42
+ # The above example will define a state machine for the attribute "state"
43
+ # on the model. Every switch will start with no initial state.
44
+ #
45
+ # With a custom attribute:
23
46
  #
24
47
  # class Switch < ActiveRecord::Base
25
- # state_machine :state, :initial => 'off' do
48
+ # state_machine :status do
26
49
  # ...
27
50
  # end
28
51
  # end
29
52
  #
30
- # With a dynamic state:
53
+ # With a static initial state:
31
54
  #
32
55
  # class Switch < ActiveRecord::Base
33
- # state_machine :state, :initial => Proc.new {|switch| (8..22).include?(Time.now.hour) ? 'on' : 'off'} do
56
+ # state_machine :status, :initial => 'off' do
34
57
  # ...
35
58
  # end
36
59
  # end
37
- def state_machine(attribute, options = {}, &block)
60
+ #
61
+ # With a dynamic initial state:
62
+ #
63
+ # class Switch < ActiveRecord::Base
64
+ # state_machine :status, :initial => Proc.new {|switch| (8..22).include?(Time.now.hour) ? 'on' : 'off'} do
65
+ # ...
66
+ # end
67
+ # end
68
+ #
69
+ # == Events and Transitions
70
+ #
71
+ # For more information about how to configure an event and its associated
72
+ # transitions, see PluginAWeek::StateMachine::Machine#event
73
+ def state_machine(*args, &block)
38
74
  unless included_modules.include?(PluginAWeek::StateMachine::InstanceMethods)
39
75
  write_inheritable_attribute :state_machines, {}
40
76
  class_inheritable_reader :state_machines
@@ -44,10 +80,12 @@ module PluginAWeek #:nodoc:
44
80
  include PluginAWeek::StateMachine::InstanceMethods
45
81
  end
46
82
 
83
+ options = args.extract_options!
84
+ attribute = args.any? ? args.first.to_s : 'state'
85
+ options[:initial] = state_machines[attribute].initial_state_without_processing if !options.include?(:initial) && state_machines[attribute]
86
+
47
87
  # This will create a new machine for subclasses as well so that the owner_class and
48
88
  # initial state can be overridden
49
- attribute = attribute.to_s
50
- options[:initial] = state_machines[attribute].initial_state_without_processing if !options.include?(:initial) && state_machines[attribute]
51
89
  machine = state_machines[attribute] = PluginAWeek::StateMachine::Machine.new(self, attribute, options)
52
90
  machine.instance_eval(&block) if block
53
91
  machine
@@ -11,16 +11,24 @@ module PluginAWeek #:nodoc:
11
11
  # The name of the action that fires the event
12
12
  attr_reader :name
13
13
 
14
+ # The list of transitions that can be made for this event
15
+ attr_reader :transitions
16
+
14
17
  delegate :owner_class,
15
18
  :to => :machine
16
19
 
17
- # Creates a new event with the given name
20
+ # Creates a new event within the context of the given machine.
21
+ #
22
+ # Configuration options:
23
+ # * +before+ - Callbacks to invoke before the event is fired
24
+ # * +after+ - Callbacks to invoke after the event is fired
18
25
  def initialize(machine, name, options = {})
19
26
  options.assert_valid_keys(:before, :after)
20
27
 
21
28
  @machine = machine
22
29
  @name = name
23
30
  @options = options.stringify_keys
31
+ @transitions = []
24
32
 
25
33
  add_transition_actions
26
34
  add_transition_callbacks
@@ -31,55 +39,60 @@ module PluginAWeek #:nodoc:
31
39
  #
32
40
  # Configuration options:
33
41
  # * +to+ - The state that being transitioned to
34
- # * +from+ - A state or array of states that can be transitioned from
42
+ # * +from+ - A state or array of states that can be transitioned from. If not specified, then the transition can occur for *any* from state
43
+ # * +except_from+ - A state or array of states that *cannot* be transitioned from.
35
44
  # * +if+ - Specifies a method, proc or string to call to determine if the validation should occur (e.g. :if => :moving?, or :if => Proc.new {|car| car.speed > 60}). The method, proc or string should return or evaluate to a true or false value.
36
45
  # * +unless+ - Specifies a method, proc or string to call to determine if the transition should not occur (e.g. :unless => :stopped?, or :unless => Proc.new {|car| car.speed <= 60}). The method, proc or string should return or evaluate to a true or false value.
37
46
  #
38
47
  # == Examples
39
48
  #
49
+ # transition :to => 'parked'
40
50
  # transition :to => 'parked', :from => 'first_gear'
41
51
  # transition :to => 'parked', :from => %w(first_gear reverse)
42
52
  # transition :to => 'parked', :from => 'first_gear', :if => :moving?
43
53
  # transition :to => 'parked', :from => 'first_gear', :unless => :stopped?
54
+ # transition :to => 'parked', :except_from => 'parked'
44
55
  def transition(options = {})
56
+ # Slice out the callback options
45
57
  options.symbolize_keys!
46
- options.assert_valid_keys(:to, :from, :if, :unless)
47
- raise ArgumentError, ':to state must be specified' unless options.include?(:to)
58
+ callback_options = {:if => options.delete(:if), :unless => options.delete(:unless)}
48
59
 
49
- # Get the states involved in the transition
50
- to_state = options.delete(:to)
51
- from_states = Array(options.delete(:from))
60
+ transition = Transition.new(self, options)
52
61
 
53
- from_states.collect do |from_state|
54
- # Create the actual transition that will update records when performed
55
- transition = Transition.new(self, from_state, to_state)
56
-
57
- # Add the callback to the model. If the callback fails, then the next
58
- # available callback for the event will run until one is successful.
59
- callback = Proc.new {|record, *args| try_transition(transition, false, record, *args)}
60
- owner_class.send("transition_on_#{name}", callback, options)
61
-
62
- # Add the callback! to the model similar to above
63
- callback = Proc.new {|record, *args| try_transition(transition, true, record, *args)}
64
- owner_class.send("transition_bang_on_#{name}", callback, options)
65
-
66
- transition
67
- end
62
+ # Add the callback to the model. If the callback fails, then the next
63
+ # available callback for the event will run until one is successful.
64
+ callback = Proc.new {|record, *args| try_transition(transition, false, record, *args)}
65
+ owner_class.send("transition_on_#{name}", callback, callback_options)
66
+
67
+ # Add the callback! to the model similar to above
68
+ callback = Proc.new {|record, *args| try_transition(transition, true, record, *args)}
69
+ owner_class.send("transition_bang_on_#{name}", callback, callback_options)
70
+
71
+ transitions << transition
72
+ transition
68
73
  end
69
74
 
70
- # Attempts to perform one of the event's transitions for the given record
75
+ # Attempts to perform one of the event's transitions for the given record.
76
+ # Any additional arguments will be passed to the event's callbacks.
71
77
  def fire(record, *args)
72
- record.class.transaction {invoke_transition_callbacks(record, false, *args) || raise(ActiveRecord::Rollback)} || false
78
+ fire_with_optional_bang(record, false, *args) || false
73
79
  end
74
80
 
75
81
  # Attempts to perform one of the event's transitions for the given record.
76
82
  # If the transition cannot be made, then a PluginAWeek::StateMachine::InvalidTransition
77
83
  # error will be raised.
78
84
  def fire!(record, *args)
79
- record.class.transaction {invoke_transition_callbacks(record, true, *args) || raise(ActiveRecord::Rollback)} || raise(PluginAWeek::StateMachine::InvalidTransition)
85
+ fire_with_optional_bang(record, true, *args) || raise(PluginAWeek::StateMachine::InvalidTransition, "Cannot transition via :#{name} from #{record.send(machine.attribute).inspect}")
80
86
  end
81
87
 
82
88
  private
89
+ # Fires the event
90
+ def fire_with_optional_bang(record, bang, *args)
91
+ record.class.transaction do
92
+ invoke_transition_callbacks(record, bang, *args) || raise(ActiveRecord::Rollback)
93
+ end
94
+ end
95
+
83
96
  # Add the various instance methods that can transition the record using
84
97
  # the current event
85
98
  def add_transition_actions
@@ -2,19 +2,41 @@ require 'state_machine/event'
2
2
 
3
3
  module PluginAWeek #:nodoc:
4
4
  module StateMachine
5
- # Represents a state machine for a particular attribute
5
+ # Represents a state machine for a particular attribute. State machines
6
+ # consist of events (a.k.a. actions) and a set of transitions that define
7
+ # how the state changes after a particular event is fired.
6
8
  #
7
- # == State callbacks
9
+ # A state machine may not necessarily know all of the possible states for
10
+ # an object since they can be any arbitrary value.
8
11
  #
9
- # These callbacks are invoked in the following order:
10
- # 1. before_exit (old state)
11
- # 2. before_enter (new state)
12
- # 3. after_exit (old state)
13
- # 4. after_enter (new state)
12
+ # == Callbacks
13
+ #
14
+ # Callbacks are supported for hooking into event calls and state transitions.
15
+ # The order in which these callbacks are invoked is shown below:
16
+ # * (1) before_exit (from state)
17
+ # * (2) before_enter (to state)
18
+ # * (3) before (event)
19
+ # * (-) update state
20
+ # * (4) after_exit (from state)
21
+ # * (5) after_enter (to state)
22
+ # * (6) after (event)
23
+ #
24
+ # == Cancelling callbacks
25
+ #
26
+ # If a <tt>before_*</tt> callback returns +false+, all the later callbacks
27
+ # and associated event are cancelled. If an <tt>after_*</tt> callback returns
28
+ # false, all the later callbacks are cancelled. Callbacks are run in the
29
+ # order in which they are defined.
30
+ #
31
+ # Note that if a <tt>before_*</tt> callback fails and the bang version of an
32
+ # event was invoked, an exception will be raised instaed of returning false.
14
33
  class Machine
15
34
  # The events that trigger transitions
16
35
  attr_reader :events
17
36
 
37
+ # A list of the states defined in the transitions of all of the events
38
+ attr_reader :states
39
+
18
40
  # The attribute for which the state machine is being defined
19
41
  attr_accessor :attribute
20
42
 
@@ -27,29 +49,30 @@ module PluginAWeek #:nodoc:
27
49
  # Creates a new state machine for the given attribute
28
50
  #
29
51
  # Configuration options:
30
- # * +initial+ - The initial value to set the attribute to
52
+ # * +initial+ - The initial value to set the attribute to. This can be an actual value or a proc, which will be evaluated at runtime.
31
53
  #
32
54
  # == Scopes
33
55
  #
34
- # This will automatically created a named scope called with_#{attribute}
56
+ # This will automatically create a named scope called with_#{attribute}
35
57
  # that will find all records that have the attribute set to a given value.
36
58
  # For example,
37
59
  #
38
60
  # Switch.with_state('on') # => Finds all switches where the state is on
39
61
  # Switch.with_states('on', 'off') # => Finds all switches where the state is either on or off
40
- def initialize(owner_class, attribute, options = {})
62
+ def initialize(owner_class, attribute = 'state', options = {})
41
63
  options.assert_valid_keys(:initial)
42
64
 
43
65
  @owner_class = owner_class
44
66
  @attribute = attribute.to_s
45
67
  @initial_state = options[:initial]
46
68
  @events = {}
69
+ @states = []
47
70
 
48
71
  add_named_scopes
49
72
  end
50
73
 
51
74
  # Gets the initial state of the machine for the given record. The record
52
- # is only used if a dynamic initial state is being used
75
+ # is only used if a dynamic initial state was configured.
53
76
  def initial_state(record)
54
77
  @initial_state.is_a?(Proc) ? @initial_state.call(record) : @initial_state
55
78
  end
@@ -60,25 +83,19 @@ module PluginAWeek #:nodoc:
60
83
  end
61
84
 
62
85
  # Defines an event of the system. This can take an optional hash that
63
- # defines callbacks which will be invoked when the object enters/exits
64
- # the event.
86
+ # defines callbacks which will be invoked before and after the event is
87
+ # invoked on the object.
65
88
  #
66
89
  # Configuration options:
67
- # * +before+ - Invoked before the event has been executed
68
- # * +after+ - Invoked after the event has been executed
69
- #
70
- # == Callback order
71
- #
72
- # These callbacks are invoked in the following order:
73
- # 1. before
74
- # 2. after
90
+ # * +before+ - One or more callbacks that will be invoked before the event has been fired
91
+ # * +after+ - One or more callbacks that will be invoked after the event has been fired
75
92
  #
76
93
  # == Instance methods
77
94
  #
78
95
  # The following instance methods are generated when a new event is defined
79
96
  # (the "park" event is used as an example):
80
- # * <tt>park(*args)</tt> - Fires the "park" event, transitioning from the current state to the next valid state. This takes an optional +args+ list which is passed to the event callbacks.
81
- # * <tt>park!(*args)</tt> - Fires the "park" event, transitioning from the current state to the next valid state. This takes an optional +args+ list which is passed to the event callbacks. If the transition cannot happen (for validation, database, etc. reasons), then an error will be raised
97
+ # * <tt>park(*args)</tt> - Fires the "park" event, transitioning from the current state to the next valid state. This takes an optional list of arguments which are passed to the event callbacks.
98
+ # * <tt>park!(*args)</tt> - Fires the "park" event, transitioning from the current state to the next valid state. This takes an optional list of arguments which are passed to the event callbacks. If the transition cannot happen (for validation, database, etc. reasons), then an error will be raised
82
99
  #
83
100
  # == Defining transitions
84
101
  #
@@ -96,6 +113,19 @@ module PluginAWeek #:nodoc:
96
113
  # See PluginAWeek::StateMachine::Event#transition for more information on
97
114
  # the possible options that can be passed in.
98
115
  #
116
+ # *Note* that this block is executed within the context of the actual event
117
+ # object. As a result, you will not be able to reference any class methods
118
+ # on the model without referencing the class itself. For example,
119
+ #
120
+ # class Car < ActiveRecord::Base
121
+ # def self.safe_states
122
+ # %w(parked idling first_gear)
123
+ # end
124
+ #
125
+ # state_machine :state do
126
+ # event :park do
127
+ # transition :to
128
+ #
99
129
  # == Example
100
130
  #
101
131
  # class Car < ActiveRecord::Base
@@ -110,6 +140,12 @@ module PluginAWeek #:nodoc:
110
140
  name = name.to_s
111
141
  event = events[name] = Event.new(self, name, options)
112
142
  event.instance_eval(&block)
143
+
144
+ # Record the states
145
+ event.transitions.each do |transition|
146
+ @states |= ([transition.to_state] + transition.from_states)
147
+ end
148
+
113
149
  event
114
150
  end
115
151
 
@@ -7,51 +7,70 @@ module PluginAWeek #:nodoc:
7
7
  # A transition indicates a state change and is described by a condition
8
8
  # that would need to be fulfilled to enable the transition. Transitions
9
9
  # consist of:
10
- # * The starting state
10
+ # * The starting state(s)
11
11
  # * The ending state
12
12
  # * A guard to check if the transition is allowed
13
13
  class Transition
14
- # The state from which the transition is being made
15
- attr_reader :from_state
16
-
17
14
  # The state to which the transition is being made
18
15
  attr_reader :to_state
19
16
 
17
+ # The states from which the transition can be made
18
+ attr_reader :from_states
19
+
20
20
  # The event that caused the transition
21
21
  attr_reader :event
22
22
 
23
23
  delegate :machine,
24
24
  :to => :event
25
25
 
26
- def initialize(event, from_state, to_state) #:nodoc:
26
+ # Creates a new transition within the context of the given event.
27
+ #
28
+ # Configuration options:
29
+ # * +to+ - The state being transitioned to
30
+ # * +from+ - One or more states being transitioned from. Default is nil (can transition from any state)
31
+ # * +except_from+ - One or more states that *can't* be transitioned from.
32
+ def initialize(event, options) #:nodoc:
27
33
  @event = event
28
- @from_state = from_state
29
- @to_state = to_state
30
- @loopback = from_state == to_state
34
+
35
+ options.assert_valid_keys(:to, :from, :except_from)
36
+ raise ArgumentError, ':to state must be specified' unless options.include?(:to)
37
+
38
+ # Get the states involved in the transition
39
+ @to_state = options[:to]
40
+ @from_states = Array(options[:from] || options[:except_from])
41
+
42
+ # Should we be matching the from states?
43
+ @require_match = !options[:from].nil?
31
44
  end
32
45
 
33
46
  # Whether or not this is a loopback transition (i.e. from and to state are the same)
34
- def loopback?(state = from_state)
35
- state == to_state
47
+ def loopback?(from_state)
48
+ from_state == to_state
36
49
  end
37
50
 
38
51
  # Determines whether or not this transition can be performed on the given
39
- # states
52
+ # record. The transition can be performed if the record's state matches
53
+ # one of the states that are valid in this transition.
40
54
  def can_perform_on?(record)
41
- !from_state || from_state == record.send(machine.attribute)
55
+ from_states.empty? || from_states.include?(record.send(machine.attribute)) == @require_match
42
56
  end
43
57
 
44
58
  # Runs the actual transition and any callbacks associated with entering
45
- # and exiting the states
59
+ # and exiting the states. Any additional arguments are passed to the
60
+ # callbacks.
61
+ #
62
+ # *Note* that the caller should check <tt>can_perform_on?</tt> before calling
63
+ # perform. This will *not* check whether transition should be performed.
46
64
  def perform(record, *args)
47
65
  perform_with_optional_bang(record, false, *args)
48
66
  end
49
67
 
50
68
  # Runs the actual transition and any callbacks associated with entering
51
69
  # and exiting the states. Any errors during validation or saving will be
52
- # raised.
70
+ # raised. If any +before+ callbacks fail, a PluginAWeek::StateMachine::InvalidTransition
71
+ # error will be raised.
53
72
  def perform!(record, *args)
54
- perform_with_optional_bang(record, true, *args) || raise(PluginAWeek::StateMachine::InvalidTransition)
73
+ perform_with_optional_bang(record, true, *args) || raise(PluginAWeek::StateMachine::InvalidTransition, "Cannot transition via :#{event.name} from #{record.send(machine.attribute).inspect} to #{to_state.inspect}")
55
74
  end
56
75
 
57
76
  private
@@ -13,6 +13,9 @@ class Switch < ActiveRecord::Base
13
13
  attr_accessor :fail_save
14
14
  before_save Proc.new {|switch| !switch.fail_save}
15
15
 
16
+ # Arbitrary data associated with the switch
17
+ attr_accessor :data
18
+
16
19
  def initialize(attributes = nil)
17
20
  @callbacks = []
18
21
  super
@@ -0,0 +1,8 @@
1
+ irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
2
+ libs = " -r irb/completion"
3
+ libs << " -r test/app_root/script/rails_framework_root"
4
+ libs << " -r test/test_helper"
5
+ libs << " -r plugin_test_helper/console_with_fixtures"
6
+ libs << " -r console_app"
7
+ libs << " -r console_with_helpers"
8
+ exec "#{irb} #{libs} --simple-prompt"
@@ -0,0 +1 @@
1
+ RAILS_FRAMEWORK_ROOT = '/home/aaron/Projects/Vendor/rails'
data/test/factory.rb CHANGED
@@ -37,12 +37,7 @@ module Factory
37
37
  end
38
38
 
39
39
  build Car do |attributes|
40
- attributes[:highway] = create_highway unless attributes.include?(:highway)
41
- attributes[:auto_shop] = create_auto_shop unless attributes.include?(:auto_shop)
42
- attributes.reverse_merge!(
43
- :seatbelt_on => false,
44
- :insurance_premium => 50
45
- )
40
+ valid_vehicle_attributes(attributes)
46
41
  end
47
42
 
48
43
  build Highway do |attributes|
@@ -62,6 +57,11 @@ module Factory
62
57
  end
63
58
 
64
59
  build Vehicle do |attributes|
65
- valid_car_attributes(attributes)
60
+ attributes[:highway] = create_highway unless attributes.include?(:highway)
61
+ attributes[:auto_shop] = create_auto_shop unless attributes.include?(:auto_shop)
62
+ attributes.reverse_merge!(
63
+ :seatbelt_on => false,
64
+ :insurance_premium => 50
65
+ )
66
66
  end
67
67
  end
@@ -14,6 +14,10 @@ class EventTest < Test::Unit::TestCase
14
14
  assert_equal 'turn_on', @event.name
15
15
  end
16
16
 
17
+ def test_should_not_have_any_transitions
18
+ assert @event.transitions.empty?
19
+ end
20
+
17
21
  def test_should_define_an_event_action_on_the_owner_class
18
22
  switch = new_switch
19
23
  assert switch.respond_to?(:turn_on)
@@ -69,12 +73,21 @@ class EventWithTransitionsTest < Test::Unit::TestCase
69
73
  assert_nothing_raised {@event.transition(:to => 'on')}
70
74
  end
71
75
 
76
+ def test_should_allow_transitioning_without_a_state
77
+ assert @event.transition(:to => 'on')
78
+ end
79
+
72
80
  def test_should_allow_transitioning_from_a_single_state
73
- assert_equal [%w(off on)], @event.transition(:to => 'on', :from => 'off').map {|t| [t.from_state, t.to_state]}
81
+ assert @event.transition(:to => 'on', :from => 'off')
74
82
  end
75
83
 
76
84
  def test_should_allow_transitioning_from_multiple_states
77
- assert_equal [%w(off on), %w(on on)], @event.transition(:to => 'on', :from => %w(off on)).map {|t| [t.from_state, t.to_state]}
85
+ assert @event.transition(:to => 'on', :from => %w(off on))
86
+ end
87
+
88
+ def test_should_have_transitions
89
+ @event.transition(:to => 'on')
90
+ assert @event.transitions.any?
78
91
  end
79
92
 
80
93
  def teardown
@@ -124,12 +137,24 @@ class EventAfterBeingFiredWithTransitionsTest < Test::Unit::TestCase
124
137
  assert_equal 'off', @switch.state
125
138
  end
126
139
 
127
- def test_should_fire_if_transition_is_matched
140
+ def test_should_fire_if_transition_with_no_from_state_is_matched
141
+ @event.transition :to => 'on'
142
+ assert @event.fire(@switch)
143
+ assert_equal 'on', @switch.state
144
+ end
145
+
146
+ def test_should_fire_if_transition_with_from_state_is_matched
128
147
  @event.transition :to => 'on', :from => 'off'
129
148
  assert @event.fire(@switch)
130
149
  assert_equal 'on', @switch.state
131
150
  end
132
151
 
152
+ def test_should_fire_if_transition_with_multiple_from_states_is_matched
153
+ @event.transition :to => 'on', :from => %w(off on)
154
+ assert @event.fire(@switch)
155
+ assert_equal 'on', @switch.state
156
+ end
157
+
133
158
  def test_should_not_fire_if_validation_failed
134
159
  @event.transition :to => 'on', :from => 'off'
135
160
  @switch.fail_validation = true
@@ -178,36 +203,59 @@ class EventAfterBeingFiredWithConditionalTransitionsTest < Test::Unit::TestCase
178
203
  def setup
179
204
  @machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
180
205
  @event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
181
-
182
- # Verify that the callbacks are being invoked correctly; should not affect
183
- # callback chain in tests
184
- @event.transition :to => 'if_not_evaluated', :from => 'off', :if => Proc.new {false}
185
- @event.transition :to => 'unless_not_evaluated', :from => 'off', :unless => Proc.new {true}
186
- @event.transition :to => 'no_record', :from => 'off', :if => Proc.new {|record, value| record.nil?}
187
- @event.transition :to => 'no_arguments', :from => 'off', :if => Proc.new {|record, value| value.nil?}
188
-
189
206
  @switch = create_switch(:state => 'off')
190
207
  end
191
208
 
192
- def test_should_not_fire_if_no_transitions_are_matched
193
- assert !@event.fire(@switch, 1)
194
- assert_equal 'off', @switch.state
209
+ def test_should_fire_if_if_is_true
210
+ @event.transition :to => 'on', :from => 'off', :if => Proc.new {true}
211
+ assert @event.fire(@switch)
195
212
  end
196
213
 
197
- def test_should_raise_exception_if_no_transitions_are_matched
198
- assert_raise(PluginAWeek::StateMachine::InvalidTransition) {@event.fire!(@switch, 1)}
199
- assert_equal 'off', @switch.state
214
+ def test_should_not_fire_if_if_is_false
215
+ @event.transition :to => 'on', :from => 'off', :if => Proc.new {false}
216
+ assert !@event.fire(@switch)
200
217
  end
201
218
 
202
- def test_should_fire_if_transition_is_matched
219
+ def test_should_fire_if_unless_is_false
220
+ @event.transition :to => 'on', :from => 'off', :unless => Proc.new {false}
221
+ assert @event.fire(@switch)
222
+ end
223
+
224
+ def test_should_not_fire_if_unless_is_true
225
+ @event.transition :to => 'on', :from => 'off', :unless => Proc.new {true}
226
+ assert !@event.fire(@switch)
227
+ end
228
+
229
+ def test_should_pass_in_record_as_argument
230
+ @event.transition :to => 'on', :from => 'off', :if => Proc.new {|record, value| !record.nil?}
231
+ assert @event.fire(@switch)
232
+ end
233
+
234
+ def test_should_pass_in_value_as_argument
203
235
  @event.transition :to => 'on', :from => 'off', :if => Proc.new {|record, value| value == 1}
204
236
  assert @event.fire(@switch, 1)
205
- assert_equal 'on', @switch.state
237
+ end
238
+
239
+ def test_should_fire_if_method_evaluates_to_true
240
+ @switch.data = true
241
+ @event.transition :to => 'on', :from => 'off', :if => :data
242
+ assert @event.fire(@switch)
243
+ end
244
+
245
+ def test_should_not_fire_if_method_evaluates_to_false
246
+ @switch.data = false
247
+ @event.transition :to => 'on', :from => 'off', :if => :data
248
+ assert !@event.fire(@switch)
249
+ end
250
+
251
+ def test_should_raise_exception_if_no_transitions_are_matched
252
+ assert_raise(PluginAWeek::StateMachine::InvalidTransition) {@event.fire!(@switch, 1)}
253
+ assert_equal 'off', @switch.state
206
254
  end
207
255
 
208
256
  def test_should_not_raise_exception_if_transition_is_matched
209
- @event.transition :to => 'on', :from => 'off', :if => Proc.new {|record, value| value == 1}
210
- assert @event.fire!(@switch, 1)
257
+ @event.transition :to => 'on', :from => 'off', :if => Proc.new {true}
258
+ assert @event.fire!(@switch)
211
259
  assert_equal 'on', @switch.state
212
260
  end
213
261
 
@@ -2,7 +2,7 @@ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
2
 
3
3
  class MachineByDefaultTest < Test::Unit::TestCase
4
4
  def setup
5
- @machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state')
5
+ @machine = PluginAWeek::StateMachine::Machine.new(Switch)
6
6
  end
7
7
 
8
8
  def test_should_have_an_attribute
@@ -20,6 +20,10 @@ class MachineByDefaultTest < Test::Unit::TestCase
20
20
  def test_should_not_have_any_events
21
21
  assert @machine.events.empty?
22
22
  end
23
+
24
+ def test_should_not_have_any_states
25
+ assert @machine.states.empty?
26
+ end
23
27
  end
24
28
 
25
29
  class MachineWithInvalidOptionsTest < Test::Unit::TestCase
@@ -121,9 +125,27 @@ class MachineWithEventsTest < Test::Unit::TestCase
121
125
  assert responded
122
126
  end
123
127
 
124
- def test_should_store_the_event
128
+ def test_should_have_events
125
129
  @machine.event(:turn_on) {}
126
- assert_equal 1, @machine.events.size
130
+ assert_equal %w(turn_on), @machine.events.keys
131
+ end
132
+ end
133
+
134
+ class MachineWithEventsAndTransitionsTest < Test::Unit::TestCase
135
+ def setup
136
+ @machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state')
137
+ @machine.event(:turn_on) do
138
+ transition :to => 'on', :from => 'off'
139
+ transition :to => 'error', :from => 'unknown'
140
+ end
141
+ end
142
+
143
+ def test_should_have_events
144
+ assert_equal %w(turn_on), @machine.events.keys
145
+ end
146
+
147
+ def test_should_have_states
148
+ assert_equal %w(on off error unknown), @machine.states
127
149
  end
128
150
  end
129
151
 
@@ -4,34 +4,39 @@ class TransitionTest < Test::Unit::TestCase
4
4
  def setup
5
5
  @machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
6
6
  @event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
7
- @transition = PluginAWeek::StateMachine::Transition.new(@event, 'off', 'on')
7
+ @transition = PluginAWeek::StateMachine::Transition.new(@event, :to => 'on')
8
8
  end
9
9
 
10
- def test_should_have_a_from_state
11
- assert_equal 'off', @transition.from_state
10
+ def test_should_not_have_any_from_states
11
+ assert @transition.from_states.empty?
12
+ end
13
+
14
+ def test_should_not_be_a_loopback_if_from_state_is_different
15
+ assert !@transition.loopback?('off')
12
16
  end
13
17
 
14
18
  def test_should_have_a_to_state
15
19
  assert_equal 'on', @transition.to_state
16
20
  end
17
21
 
18
- def test_should_not_be_a_loopback
19
- assert !@transition.loopback?
22
+ def test_should_be_loopback_if_from_state_is_same
23
+ assert @transition.loopback?('on')
20
24
  end
21
25
 
22
- def test_should_not_be_able_to_perform_if_record_state_is_not_from_state
23
- record = new_switch(:state => 'on')
24
- assert !@transition.can_perform_on?(record)
25
- end
26
-
27
- def test_should_be_able_to_perform_if_record_state_is_from_state
26
+ def test_should_be_able_to_perform_on_all_states
28
27
  record = new_switch(:state => 'off')
29
28
  assert @transition.can_perform_on?(record)
29
+
30
+ record = new_switch(:state => 'on')
31
+ assert @transition.can_perform_on?(record)
30
32
  end
31
33
 
32
- def test_should_perform_for_valid_from_state
34
+ def test_should_perform_for_all_states
33
35
  record = new_switch(:state => 'off')
34
36
  assert @transition.perform(record)
37
+
38
+ record = new_switch(:state => 'on')
39
+ assert @transition.perform(record)
35
40
  end
36
41
 
37
42
  def test_should_not_raise_exception_if_not_valid_during_perform
@@ -61,68 +66,131 @@ class TransitionTest < Test::Unit::TestCase
61
66
 
62
67
  assert_raise(ActiveRecord::RecordNotSaved) {@transition.perform!(record)}
63
68
  end
69
+
70
+ def test_should_raise_exception_if_invalid_option_specified
71
+ assert_raise(ArgumentError) {PluginAWeek::StateMachine::Transition.new(@event, :invalid => true)}
72
+ end
73
+
74
+ def test_should_raise_exception_if_to_option_not_specified
75
+ assert_raise(ArgumentError) {PluginAWeek::StateMachine::Transition.new(@event, :from => 'off')}
76
+ end
64
77
  end
65
78
 
66
- class TransitionWithoutFromStateTest < Test::Unit::TestCase
79
+ class TransitionWithLoopbackTest < Test::Unit::TestCase
67
80
  def setup
68
81
  @machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
69
82
  @event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
70
- @transition = PluginAWeek::StateMachine::Transition.new(@event, nil, 'on')
83
+ @transition = PluginAWeek::StateMachine::Transition.new(@event, :to => 'on', :from => 'on')
71
84
  end
72
85
 
73
- def test_should_not_have_a_from_state
74
- assert_nil @transition.from_state
86
+ def test_should_be_able_to_perform
87
+ record = new_switch(:state => 'on')
88
+ assert @transition.can_perform_on?(record)
75
89
  end
76
90
 
77
- def test_should_be_able_to_perform_on_all_states
78
- record = new_switch(:state => 'off')
79
- assert @transition.can_perform_on?(record)
80
-
91
+ def test_should_perform_for_valid_from_state
81
92
  record = new_switch(:state => 'on')
82
- assert @transition.can_perform_on?(record)
93
+ assert @transition.perform(record)
83
94
  end
84
95
  end
85
96
 
86
- class TransitionWithLoopbackTest < Test::Unit::TestCase
97
+ class TransitionWithFromStateTest < Test::Unit::TestCase
87
98
  def setup
88
99
  @machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
89
100
  @event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
90
- @transition = PluginAWeek::StateMachine::Transition.new(@event, 'on', 'on')
101
+ @transition = PluginAWeek::StateMachine::Transition.new(@event, :to => 'on', :from => 'off')
91
102
  end
92
103
 
93
104
  def test_should_have_a_from_state
94
- assert_equal 'on', @transition.from_state
105
+ assert_equal ['off'], @transition.from_states
95
106
  end
96
107
 
97
- def test_should_have_a_to_state
98
- assert_equal 'on', @transition.to_state
108
+ def test_should_not_be_able_to_perform_if_record_state_is_not_from_state
109
+ record = new_switch(:state => 'on')
110
+ assert !@transition.can_perform_on?(record)
99
111
  end
100
112
 
101
- def test_should_be_a_loopback
102
- assert @transition.loopback?
113
+ def test_should_be_able_to_perform_if_record_state_is_from_state
114
+ record = new_switch(:state => 'off')
115
+ assert @transition.can_perform_on?(record)
103
116
  end
104
117
 
105
- def test_should_not_be_able_to_perform_if_record_state_is_not_from_state
118
+ def test_should_perform_for_valid_from_state
106
119
  record = new_switch(:state => 'off')
120
+ assert @transition.perform(record)
121
+ end
122
+ end
123
+
124
+ class TransitionWithMultipleFromStatesTest < Test::Unit::TestCase
125
+ def setup
126
+ @machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
127
+ @event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
128
+ @transition = PluginAWeek::StateMachine::Transition.new(@event, :to => 'on', :from => %w(off on))
129
+ end
130
+
131
+ def test_should_have_multiple_from_states
132
+ assert_equal ['off', 'on'], @transition.from_states
133
+ end
134
+
135
+ def test_should_not_be_able_to_perform_if_record_state_is_not_from_state
136
+ record = new_switch(:state => 'unknown')
107
137
  assert !@transition.can_perform_on?(record)
108
138
  end
109
139
 
110
- def test_should_be_able_to_perform_if_record_is_in_from_state
140
+ def test_should_be_able_to_perform_if_record_state_is_any_from_state
141
+ record = new_switch(:state => 'off')
142
+ assert @transition.can_perform_on?(record)
143
+
111
144
  record = new_switch(:state => 'on')
112
145
  assert @transition.can_perform_on?(record)
113
146
  end
114
147
 
115
- def test_should_perform_for_valid_from_state
148
+ def test_should_perform_for_any_valid_from_state
149
+ record = new_switch(:state => 'off')
150
+ assert @transition.perform(record)
151
+
116
152
  record = new_switch(:state => 'on')
117
153
  assert @transition.perform(record)
118
154
  end
119
155
  end
120
156
 
157
+ class TransitionWithMismatchedFromStatesRequiredTest < Test::Unit::TestCase
158
+ def setup
159
+ @machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
160
+ @event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
161
+ @transition = PluginAWeek::StateMachine::Transition.new(@event, :to => 'on', :except_from => 'on')
162
+ end
163
+
164
+ def test_should_have_a_from_state
165
+ assert_equal ['on'], @transition.from_states
166
+ end
167
+
168
+ def test_should_be_able_to_perform_if_record_state_is_not_from_state
169
+ record = new_switch(:state => 'off')
170
+ assert @transition.can_perform_on?(record)
171
+ end
172
+
173
+ def test_should_not_be_able_to_perform_if_record_state_is_from_state
174
+ record = new_switch(:state => 'on')
175
+ assert !@transition.can_perform_on?(record)
176
+ end
177
+
178
+ def test_should_perform_for_valid_from_state
179
+ record = new_switch(:state => 'off')
180
+ assert @transition.perform(record)
181
+ end
182
+
183
+ def test_should_not_perform_for_invalid_from_state
184
+ record = new_switch(:state => 'on')
185
+ assert !@transition.can_perform_on?(record)
186
+ end
187
+ end
188
+
121
189
  class TransitionAfterBeingPerformedTest < Test::Unit::TestCase
122
190
  def setup
123
191
  @machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
124
192
  @event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
125
- @transition = PluginAWeek::StateMachine::Transition.new(@event, 'off', 'on')
193
+ @transition = PluginAWeek::StateMachine::Transition.new(@event, :to => 'on', :from => 'off')
126
194
 
127
195
  @record = create_switch(:state => 'off')
128
196
  @transition.perform(@record)
@@ -142,7 +210,7 @@ class TransitionWithLoopbackAfterBeingPerformedTest < Test::Unit::TestCase
142
210
  def setup
143
211
  @machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
144
212
  @event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
145
- @transition = PluginAWeek::StateMachine::Transition.new(@event, 'on', 'on')
213
+ @transition = PluginAWeek::StateMachine::Transition.new(@event, :to => 'on', :from => 'on')
146
214
 
147
215
  @record = create_switch(:state => 'on')
148
216
  @transition.perform(@record)
@@ -162,7 +230,7 @@ class TransitionWithCallbacksTest < Test::Unit::TestCase
162
230
  def setup
163
231
  @machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
164
232
  @event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
165
- @transition = PluginAWeek::StateMachine::Transition.new(@event, 'off', 'on')
233
+ @transition = PluginAWeek::StateMachine::Transition.new(@event, :to => 'on', :from => 'off')
166
234
  @record = create_switch(:state => 'off')
167
235
 
168
236
  Switch.define_callbacks :before_exit_state_off, :before_enter_state_on, :after_exit_state_off, :after_enter_state_on
@@ -295,7 +363,7 @@ class TransitionWithoutFromStateAndCallbacksTest < Test::Unit::TestCase
295
363
  def setup
296
364
  @machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
297
365
  @event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
298
- @transition = PluginAWeek::StateMachine::Transition.new(@event, nil, 'on')
366
+ @transition = PluginAWeek::StateMachine::Transition.new(@event, :to => 'on')
299
367
  @record = create_switch(:state => 'off')
300
368
 
301
369
  Switch.define_callbacks :before_exit_state_off, :before_enter_state_on, :after_exit_state_off, :after_enter_state_on
@@ -397,7 +465,7 @@ class TransitionWithLoopbackAndCallbacksTest < Test::Unit::TestCase
397
465
  def setup
398
466
  @machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
399
467
  @event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
400
- @transition = PluginAWeek::StateMachine::Transition.new(@event, 'on', 'on')
468
+ @transition = PluginAWeek::StateMachine::Transition.new(@event, :to => 'on', :from => 'on')
401
469
  @record = create_switch(:state => 'on')
402
470
 
403
471
  Switch.define_callbacks :before_exit_state_off, :before_enter_state_on, :after_exit_state_off, :after_enter_state_on
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: state_machine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Pfeifer
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-06-29 00:00:00 -04:00
12
+ date: 2008-07-05 00:00:00 -04:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -37,6 +37,9 @@ files:
37
37
  - test/app_root/app/models/vehicle.rb
38
38
  - test/app_root/app/models/car.rb
39
39
  - test/app_root/app/models/auto_shop.rb
40
+ - test/app_root/script
41
+ - test/app_root/script/rails_framework_root.rb
42
+ - test/app_root/script/console
40
43
  - test/app_root/db
41
44
  - test/app_root/db/migrate
42
45
  - test/app_root/db/migrate/002_create_auto_shops.rb