statemachine 0.0.3 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGES CHANGED
@@ -1,5 +1,20 @@
1
1
  = StateMachine Changelog
2
2
 
3
+ == Version 0.1.0
4
+
5
+ A new way to build the statemachines
6
+ * cleaner API for running a statemachine
7
+ * much refactoring
8
+ * new API for building statemachine
9
+ * process_event accepts strings
10
+
11
+ == Version 0.0.4
12
+
13
+ Some minor improvements
14
+ * Proper handling of state transition implemented, such that the proper state is set for entry and exit actions.
15
+ * can now use State objects in addition to symbols while creating a transition
16
+ * more compliant implementation of history state
17
+
3
18
  == Version 0.0.3
4
19
 
5
20
  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
data/TODO ADDED
@@ -0,0 +1,4 @@
1
+ Allow setting of start state at beginning
2
+ Remove proc actions in favor of method names. Statemachine will have a context in which methods will run.
3
+ Implement default history
4
+ Implement superstate end state with automatic transition
data/lib/statemachine.rb CHANGED
@@ -1,2 +1,7 @@
1
+ require 'statemachine/state'
2
+ require 'statemachine/super_state'
3
+ require 'statemachine/transition'
4
+ require 'statemachine/proc_calling'
1
5
  require 'statemachine/state_machine'
6
+ require 'statemachine/builder'
2
7
  require 'statemachine/version'
@@ -0,0 +1,111 @@
1
+ module StateMachine
2
+
3
+ def self.build(statemachine = nil)
4
+ builder = statemachine ? StatemachineBuilder.new(statemachine) : StatemachineBuilder.new
5
+ yield builder
6
+ builder.statemachine.reset
7
+ return builder.statemachine
8
+ end
9
+
10
+ class Builder
11
+ attr_reader :statemachine
12
+
13
+ def initialize(statemachine)
14
+ @statemachine = statemachine
15
+ end
16
+
17
+ protected
18
+ def acquire_state_in(state_id, context)
19
+ return nil if state_id == nil
20
+ return state_id if state_id.is_a? State
21
+ state = nil
22
+ if @statemachine.has_state(state_id)
23
+ state = @statemachine.get_state(state_id)
24
+ else
25
+ state = State.new(state_id, context, @statemachine)
26
+ @statemachine.add_state(state)
27
+ end
28
+ context.start_state = state if context.start_state == nil
29
+ return state
30
+ end
31
+ end
32
+
33
+ module StateBuilding
34
+ attr_reader :subject
35
+
36
+ def event(event, destination_id, action = nil)
37
+ @subject.add(Transition.new(@subject.id, destination_id, event, action))
38
+ end
39
+
40
+ def on_entry(&entry_action)
41
+ @subject.entry_action = entry_action
42
+ end
43
+
44
+ def on_exit(&exit_action)
45
+ @subject.exit_action = exit_action
46
+ end
47
+ end
48
+
49
+ module SuperstateBuilding
50
+ attr_reader :subject
51
+
52
+ def state(id)
53
+ builder = StateBuilder.new(id, @subject, @statemachine)
54
+ yield builder
55
+ end
56
+
57
+ def superstate(id)
58
+ builder = SuperstateBuilder.new(id, @subject, @statemachine)
59
+ yield builder
60
+ end
61
+
62
+ def trans(origin_id, event, destination_id, action = nil)
63
+ origin = acquire_state_in(origin_id, @subject)
64
+ origin.add(Transition.new(origin_id, destination_id, event, action))
65
+ end
66
+
67
+ def start_state(start_state_id)
68
+ @subject.start_state = @statemachine.get_state(start_state_id)
69
+ raise "Start state #{start_state_id} not found" if not @subject.start_state
70
+ end
71
+
72
+ def on_entry_of(id, &action)
73
+ @statemachine.get_state(id).entry_action = action
74
+ end
75
+
76
+ def on_exit_of(id, &action)
77
+ @statemachine.get_state(id).exit_action = action
78
+ end
79
+ end
80
+
81
+ class StateBuilder < Builder
82
+ include StateBuilding
83
+
84
+ def initialize(id, superstate, statemachine)
85
+ super statemachine
86
+ @subject = acquire_state_in(id, superstate)
87
+ end
88
+ end
89
+
90
+ class SuperstateBuilder < Builder
91
+ include StateBuilding
92
+ include SuperstateBuilding
93
+
94
+ def initialize(id, superstate, statemachine)
95
+ super statemachine
96
+ @subject = Superstate.new(id, superstate, statemachine)
97
+ superstate.start_state = @subject if superstate.start_state == nil
98
+ statemachine.add_state(@subject)
99
+ end
100
+ end
101
+
102
+ class StatemachineBuilder < Builder
103
+ include SuperstateBuilding
104
+
105
+ def initialize(statemachine = StateMachine.new)
106
+ super statemachine
107
+ @subject = @statemachine.root
108
+ end
109
+ end
110
+
111
+ end
@@ -6,47 +6,12 @@ module StateMachine
6
6
 
7
7
  def call_proc(proc, args, message)
8
8
  arity = proc.arity
9
- if should_call_with(arity, 0, args, message)
10
- proc.call
11
- elsif should_call_with(arity, 1, args, message)
12
- proc.call args[0]
13
- elsif should_call_with(arity, 2, args, message)
14
- proc.call args[0], args[1]
15
- elsif should_call_with(arity, 3, args, message)
16
- proc.call args[0], args[1], args[2]
17
- elsif should_call_with(arity, 4, args, message)
18
- proc.call args[0], args[1], args[2], args[3]
19
- elsif should_call_with(arity, 5, args, message)
20
- proc.call args[0], args[1], args[2], args[3], args[4]
21
- elsif should_call_with(arity, 6, args, message)
22
- proc.call args[0], args[1], args[2], args[3], args[4], args[5]
23
- elsif should_call_with(arity, 7, args, message)
24
- proc.call args[0], args[1], args[2], args[3], args[4], args[5], args[6]
25
- elsif should_call_with(arity, 8, args, message)
26
- proc.call args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]
27
- elsif arity < 0 and args and args.length > 8
28
- proc.call args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]
29
- else
30
- raise StateMachineException.new("Too many arguments(#{args.length}). (#{message})")
31
- end
32
- end
33
-
34
- def should_call_with(arity, n, args, message)
35
- actual = args ? args.length : 0
36
- if arity == n
37
- return enough_args?(actual, arity, arity, message)
38
- elsif arity < 0
39
- required_args = (arity * -1) - 1
40
- return (actual == n and enough_args?(actual, required_args, arity, message))
41
- end
42
- end
43
-
44
- def enough_args?(actual, required, arity, message)
45
- if actual >= required
46
- return true
47
- else
48
- raise StateMachineException.new("Insufficient parameters. (#{message})")
49
- end
9
+ required_params = arity < 0 ? arity.abs - 1 : arity
10
+
11
+ raise StateMachineException.new("Insufficient parameters. (#{message})") if required_params > args.length
12
+
13
+ parameters = arity < 0 ? args : args[0...arity]
14
+ proc.call(*parameters)
50
15
  end
51
16
 
52
17
  end
@@ -6,11 +6,12 @@ module StateMachine
6
6
 
7
7
  include ProcCalling
8
8
 
9
- attr_reader :id, :statemachine, :entry_action, :exit_action
10
- attr_accessor :superstate
9
+ attr_reader :id, :statemachine, :superstate
10
+ attr_accessor :entry_action, :exit_action
11
11
 
12
- def initialize(id, state_machine)
12
+ def initialize(id, superstate, state_machine)
13
13
  @id = id
14
+ @superstate = superstate
14
15
  @statemachine = state_machine
15
16
  @transitions = {}
16
17
  end
@@ -22,33 +23,15 @@ module StateMachine
22
23
  def transitions
23
24
  return @superstate ? @transitions.merge(@superstate.transitions) : @transitions
24
25
  end
25
-
26
- def local_transitions
27
- return @transitions
28
- end
29
-
30
- def [] (event)
31
- return transitions[event]
32
- end
33
-
34
- def on_entry action
35
- @entry_action = action
36
- end
37
-
38
- def on_exit action
39
- @exit_action = action
40
- end
41
26
 
42
27
  def exit(args)
43
28
  @statemachine.trace("\texiting #{self}")
44
- activate
45
29
  call_proc(@exit_action, args, "exit action for #{self}") if @exit_action
46
- @superstate.existing(self) if @superstate
30
+ @superstate.substate_exiting(self) if @superstate
47
31
  end
48
32
 
49
33
  def enter(args)
50
34
  @statemachine.trace("\tentering #{self}")
51
- activate
52
35
  call_proc(@entry_action, args, "entry action for #{self}") if @entry_action
53
36
  end
54
37
 
@@ -56,20 +39,14 @@ module StateMachine
56
39
  @statemachine.state = self
57
40
  end
58
41
 
59
- def is_superstate?
60
- return false
42
+ def is_concrete?
43
+ return true
61
44
  end
62
45
 
63
46
  def to_s
64
47
  return "'#{id}' state"
65
48
  end
66
49
 
67
- def add_substates(*substate_ids)
68
- raise StateMachineException.new("At least one parameter is required for add_substates.") if substate_ids.length == 0
69
- replacement = Superstate.new(self, @transitions, substate_ids)
70
- @statemachine.replace_state(@id, replacement)
71
- end
72
-
73
50
  end
74
51
 
75
52
  end
@@ -1,44 +1,33 @@
1
- require 'statemachine/state'
2
- require 'statemachine/super_state'
3
- require 'statemachine/transition'
4
- require 'statemachine/proc_calling'
5
-
6
1
  module StateMachine
7
2
 
8
3
  class StateMachineException < Exception
9
4
  end
10
5
 
11
- class MissingTransitionException < StateMachineException
12
- end
13
-
14
6
  class StateMachine
15
-
16
7
  include ProcCalling
17
8
 
18
- attr_reader :states, :state
19
- attr_accessor :start_state, :tracer
9
+ attr_accessor :tracer
10
+ attr_reader :root
20
11
 
21
- def initialize
12
+ def initialize(root = Superstate.new(:root, nil, self))
13
+ @root = root
22
14
  @states = {}
23
- @start_state = nil
24
- @state = nil
25
- @running = false
26
15
  end
27
-
28
- def add(origin_id, event, destination_id, action = nil)
29
- origin = acquire_state(origin_id)
30
- @start_state = origin if @start_state == nil
31
- destination = acquire_state(destination_id)
32
- origin.add(Transition.new(origin, destination, event, action))
16
+
17
+ def start_state
18
+ return @root.start_state.id
33
19
  end
34
20
 
35
- def run
36
- @state = @start_state
21
+ def reset
22
+ @state = @root.start_state
23
+ while @state and not @state.is_concrete?
24
+ @state = @state.start_state
25
+ end
26
+ raise StateMachineException.new("The state machine doesn't know where to start. Try setting the start_state.") if @state == nil
37
27
  end
38
- alias :reset :run
39
-
40
- def [] (state_id)
41
- return @states[state_id]
28
+
29
+ def state
30
+ return @state.id
42
31
  end
43
32
 
44
33
  def state= value
@@ -50,54 +39,74 @@ module StateMachine
50
39
  @state = @states[value.to_sym]
51
40
  end
52
41
  end
53
-
42
+
54
43
  def process_event(event, *args)
44
+ event = event.to_sym
55
45
  trace "Event: #{event}"
56
46
  if @state
57
47
  transition = @state.transitions[event]
58
48
  if transition
59
- transition.invoke(@state, args)
49
+ transition.invoke(@state, self, args)
60
50
  else
61
- raise MissingTransitionException.new("#{@state} does not respond to the '#{event}' event.")
51
+ raise StateMachineException.new("#{@state} does not respond to the '#{event}' event.")
62
52
  end
63
53
  else
64
- raise StateMachineException.new("The state machine isn't in any state. Did you forget to call run?")
54
+ raise StateMachineException.new("The state machine isn't in any state while processing the '#{event}' event.")
65
55
  end
66
56
  end
67
-
57
+
58
+ def trace(message)
59
+ @tracer.puts message if @tracer
60
+ end
61
+
62
+ def get_state(id)
63
+ if @states.has_key? id
64
+ return @states[id]
65
+ elsif(is_history_state_id?(id))
66
+ superstate_id = base_id(id)
67
+ superstate = @states[superstate_id]
68
+ raise StateMachineException.new("No history exists for #{superstate} since it is not a super state.") if superstate.is_concrete?
69
+ raise StateMachineException.new("#{superstate} doesn't have any history yet.") if not superstate.history
70
+ return superstate.history
71
+ else
72
+ state = State.new(id, @root, self)
73
+ @states[id] = state
74
+ return state
75
+ end
76
+ end
77
+
78
+ def add_state(state)
79
+ @states[state.id] = state
80
+ end
81
+
82
+ def has_state(id)
83
+ if(is_history_state_id?(id))
84
+ return @states.has_key?(base_id(id))
85
+ else
86
+ return @states.has_key?(id)
87
+ end
88
+ end
89
+
68
90
  def method_missing(message, *args)
69
- if @state and @state[message]
91
+ if @state and @state.transitions[message]
70
92
  method = self.method(:process_event)
71
93
  params = [message.to_sym].concat(args)
72
- call_proc(method, params, "method_missing")
94
+ method.call(*params)
73
95
  else
74
96
  super(message, args)
75
97
  end
76
98
  end
77
-
78
- def acquire_state(state_id)
79
- return nil if state_id == nil
80
- state = @states[state_id]
81
- if not state
82
- state = State.new(state_id, self)
83
- @states[state_id] = state
84
- end
85
- return state
86
- end
87
99
 
88
- def replace_state(state_id, replacement_state)
89
- @states[state_id] = replacement_state
90
- @states.values.each do |state|
91
- state.local_transitions.values.each do |transition|
92
- transition.destination = replacement_state if transition.destination.id == state_id
93
- end
94
- end
100
+ private
101
+
102
+ def is_history_state_id?(id)
103
+ return id.to_s[-2..-1] == "_H"
95
104
  end
96
105
 
97
- def trace(message)
98
- @tracer.puts message if @tracer
106
+ def base_id(history_id)
107
+ return history_id.to_s[0...-2].to_sym
99
108
  end
100
-
109
+
101
110
  end
102
111
 
103
112
  end
@@ -3,65 +3,33 @@ module StateMachine
3
3
  class Superstate < State
4
4
 
5
5
  attr_writer :start_state
6
+ attr_reader :history
6
7
 
7
- def initialize(state, transitions, substate_ids)
8
- @id = state.id
9
- @statemachine = state.statemachine
10
- @transitions = transitions
11
- @entry_action = state.entry_action
12
- @exit_action = state.exit_action
13
- @superstate = state.superstate
14
- do_substate_adding(substate_ids)
8
+ def initialize(id, superstate, statemachine)
9
+ super(id, superstate, statemachine)
10
+ @start_state = nil
11
+ @history = nil
15
12
  end
16
13
 
17
- def is_superstate?
18
- return true
14
+ def is_concrete?
15
+ return false
19
16
  end
20
17
 
21
18
  def start_state
22
- if @use_history and @history_state
23
- return @history_state
24
- else
25
- return @start_state
26
- end
19
+ return @start_state
27
20
  end
28
21
 
29
- def existing(substate)
30
- @history_state = substate
22
+ def substate_exiting(substate)
23
+ @history = substate
31
24
  end
32
25
 
33
26
  def add_substates(*substate_ids)
34
27
  do_substate_adding(substate_ids)
35
28
  end
36
-
37
- def use_history
38
- @use_history = true;
39
- end
40
29
 
41
30
  def to_s
42
31
  return "'#{id}' superstate"
43
32
  end
44
-
45
- private
46
-
47
- def do_substate_adding(substate_ids)
48
- substate_ids.each do |substate_id|
49
- substate = @statemachine.acquire_state(substate_id)
50
- @start_state = substate if not @start_state
51
- substate.superstate = self
52
- check_for_substate_recursion
53
- end
54
- end
55
-
56
- def check_for_substate_recursion
57
- tmp_state = @superstate
58
- while tmp_state
59
- if tmp_state == self
60
- raise StateMachineException.new("Cyclic substates not allowed. (#{id})")
61
- end
62
- tmp_state = tmp_state.superstate
63
- end
64
- end
65
33
 
66
34
  end
67
35