statemachine 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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