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 +15 -0
- data/TODO +4 -0
- data/lib/statemachine.rb +5 -0
- data/lib/statemachine/builder.rb +111 -0
- data/lib/statemachine/proc_calling.rb +6 -41
- data/lib/statemachine/state.rb +7 -30
- data/lib/statemachine/state_machine.rb +63 -54
- data/lib/statemachine/super_state.rb +10 -42
- data/lib/statemachine/transition.rb +23 -21
- data/lib/statemachine/version.rb +2 -2
- data/spec/builder_spec.rb +131 -0
- data/spec/sm_action_parameterization_spec.rb +38 -53
- data/spec/sm_entry_exit_actions_spec.rb +35 -27
- data/spec/sm_odds_n_ends_spec.rb +47 -5
- data/spec/sm_simple_spec.rb +3 -36
- data/spec/sm_super_state_spec.rb +13 -54
- data/spec/sm_turnstile_spec.rb +18 -20
- data/spec/spec_helper.rb +15 -11
- data/spec/transition_spec.rb +68 -58
- metadata +7 -3
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
data/lib/statemachine.rb
CHANGED
@@ -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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
data/lib/statemachine/state.rb
CHANGED
@@ -6,11 +6,12 @@ module StateMachine
|
|
6
6
|
|
7
7
|
include ProcCalling
|
8
8
|
|
9
|
-
attr_reader :id, :statemachine, :
|
10
|
-
attr_accessor :
|
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.
|
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
|
60
|
-
return
|
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
|
-
|
19
|
-
|
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
|
29
|
-
|
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
|
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
|
-
|
39
|
-
|
40
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
98
|
-
|
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(
|
8
|
-
|
9
|
-
@
|
10
|
-
@
|
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
|
18
|
-
return
|
14
|
+
def is_concrete?
|
15
|
+
return false
|
19
16
|
end
|
20
17
|
|
21
18
|
def start_state
|
22
|
-
|
23
|
-
return @history_state
|
24
|
-
else
|
25
|
-
return @start_state
|
26
|
-
end
|
19
|
+
return @start_state
|
27
20
|
end
|
28
21
|
|
29
|
-
def
|
30
|
-
@
|
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
|
|