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 +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
|
|