pushdown 0.1.0.pre.20210714190141 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +2 -0
- data/History.md +6 -1
- data/LICENSE.txt +20 -0
- data/lib/pushdown/automaton.rb +236 -0
- data/lib/pushdown/exceptions.rb +18 -0
- data/lib/pushdown/state.rb +110 -0
- data/lib/pushdown/transition/pop.rb +35 -0
- data/lib/pushdown/transition/push.rb +41 -0
- data/lib/pushdown/transition/replace.rb +43 -0
- data/lib/pushdown/transition/switch.rb +41 -0
- data/lib/pushdown/transition.rb +85 -0
- data/lib/pushdown.rb +5 -5
- data/spec/pushdown/automaton_spec.rb +151 -0
- data/spec/pushdown/state_spec.rb +88 -0
- data/spec/pushdown/transition/pop_spec.rb +67 -0
- data/spec/pushdown/transition/push_spec.rb +53 -0
- data/spec/pushdown/transition/replace_spec.rb +57 -0
- data/spec/pushdown/transition/switch_spec.rb +52 -0
- data/spec/pushdown/transition_spec.rb +91 -0
- data/spec/spec_helper.rb +4 -0
- data.tar.gz.sig +0 -0
- metadata +91 -10
- metadata.gz.sig +0 -0
- data/.simplecov +0 -9
- data/Rakefile +0 -8
@@ -0,0 +1,41 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'pushdown/transition' unless defined?( Pushdown::Transition )
|
5
|
+
|
6
|
+
|
7
|
+
# A push transition -- add an instance of a given State to the top of the state
|
8
|
+
# stack.
|
9
|
+
class Pushdown::Transition::Push < Pushdown::Transition
|
10
|
+
|
11
|
+
|
12
|
+
### Create a transition that will Push an instance of the given +state_class+ to
|
13
|
+
### the stack.
|
14
|
+
def initialize( name, state_class, *args )
|
15
|
+
super( name, *args )
|
16
|
+
@state_class = state_class
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
######
|
21
|
+
public
|
22
|
+
######
|
23
|
+
|
24
|
+
##
|
25
|
+
# The State to push to.
|
26
|
+
attr_reader :state_class
|
27
|
+
|
28
|
+
|
29
|
+
### Apply the transition to the given +stack+.
|
30
|
+
def apply( stack )
|
31
|
+
state = self.state_class.new
|
32
|
+
|
33
|
+
self.log.debug "pushing a new state: %p" % [ state ]
|
34
|
+
self.data = stack.last.on_pause( self.data ) if stack.last
|
35
|
+
stack.push( state )
|
36
|
+
state.on_start( self.data )
|
37
|
+
|
38
|
+
return stack
|
39
|
+
end
|
40
|
+
|
41
|
+
end # class Pushdown::Transition::Push
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'pushdown/transition' unless defined?( Pushdown::Transition )
|
5
|
+
|
6
|
+
|
7
|
+
# A replace transition -- remove all currents states from the stack and add a
|
8
|
+
# different one.
|
9
|
+
class Pushdown::Transition::Replace < Pushdown::Transition
|
10
|
+
|
11
|
+
### Create a transition that will Replace all the states on the current stack
|
12
|
+
### with an instance of the given +state_class+.
|
13
|
+
def initialize( name, state_class, *args )
|
14
|
+
super( name, *args )
|
15
|
+
@state_class = state_class
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
######
|
20
|
+
public
|
21
|
+
######
|
22
|
+
|
23
|
+
##
|
24
|
+
# The State to replace the stack members with.
|
25
|
+
attr_reader :state_class
|
26
|
+
|
27
|
+
|
28
|
+
### Apply the transition to the given +stack+.
|
29
|
+
def apply( stack )
|
30
|
+
state = self.state_class.new
|
31
|
+
|
32
|
+
self.log.debug "replacing current state with a new state: %p" % [ state ]
|
33
|
+
while ( old_state = stack.pop )
|
34
|
+
self.data = old_state.on_stop( self.data )
|
35
|
+
end
|
36
|
+
|
37
|
+
stack.push( state )
|
38
|
+
state.on_start( self.data )
|
39
|
+
|
40
|
+
return stack
|
41
|
+
end
|
42
|
+
|
43
|
+
end # class Pushdown::Transition::Replace
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'pushdown/transition' unless defined?( Pushdown::Transition )
|
5
|
+
|
6
|
+
|
7
|
+
# A switch transition -- remove the current state from the stack and add a
|
8
|
+
# different one.
|
9
|
+
class Pushdown::Transition::Switch < Pushdown::Transition
|
10
|
+
|
11
|
+
### Create a transition that will Switch the current State with an instance of
|
12
|
+
### the given +state_class+ on the stack.
|
13
|
+
def initialize( name, state_class, *args )
|
14
|
+
super( name, *args )
|
15
|
+
@state_class = state_class
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
######
|
20
|
+
public
|
21
|
+
######
|
22
|
+
|
23
|
+
##
|
24
|
+
# The State to push to.
|
25
|
+
attr_reader :state_class
|
26
|
+
|
27
|
+
|
28
|
+
### Apply the transition to the given +stack+.
|
29
|
+
def apply( stack )
|
30
|
+
state = self.state_class.new
|
31
|
+
|
32
|
+
self.log.debug "switching current state with a new state: %p" % [ state ]
|
33
|
+
old_state = stack.pop
|
34
|
+
self.data = old_state.on_stop( self.data )
|
35
|
+
stack.push( state )
|
36
|
+
state.on_start( self.data )
|
37
|
+
|
38
|
+
return stack
|
39
|
+
end
|
40
|
+
|
41
|
+
end # class Pushdown::Transition::Switch
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'loggability'
|
5
|
+
require 'pluggability'
|
6
|
+
|
7
|
+
require 'pushdown' unless defined?( Pushdown )
|
8
|
+
|
9
|
+
# pub enum Trans<T, E> {
|
10
|
+
# /// Continue as normal.
|
11
|
+
# None,
|
12
|
+
# /// Remove the active state and resume the next state on the stack or stop
|
13
|
+
# /// if there are none.
|
14
|
+
# Pop,
|
15
|
+
# /// Pause the active state and push a new state onto the stack.
|
16
|
+
# Push(Box<dyn State<T, E>>),
|
17
|
+
# /// Remove the current state on the stack and insert a different one.
|
18
|
+
# Switch(Box<dyn State<T, E>>),
|
19
|
+
# /// Remove all states on the stack and insert a different one.
|
20
|
+
# Replace(Box<dyn State<T, E>>),
|
21
|
+
# /// Remove all states on the stack and insert new stack.
|
22
|
+
# NewStack(Vec<Box<dyn State<T, E>>>),
|
23
|
+
# /// Execute a series of Trans's.
|
24
|
+
# Sequence(Vec<Trans<T, E>>),
|
25
|
+
# /// Stop and remove all states and shut down the engine.
|
26
|
+
# Quit,
|
27
|
+
# }
|
28
|
+
|
29
|
+
# A transition in a Pushdown automaton
|
30
|
+
class Pushdown::Transition
|
31
|
+
extend Loggability,
|
32
|
+
Pluggability
|
33
|
+
|
34
|
+
# Loggability API -- log to the pushdown logger
|
35
|
+
log_to :pushdown
|
36
|
+
|
37
|
+
# Pluggability API -- concrete types live in lib/pushdown/transition/
|
38
|
+
plugin_prefixes 'pushdown/transition/'
|
39
|
+
plugin_exclusions 'spec/**/*'
|
40
|
+
|
41
|
+
|
42
|
+
# Don't allow direct instantiation (abstract class)
|
43
|
+
private_class_method :new
|
44
|
+
|
45
|
+
|
46
|
+
### Inheritance hook -- enable instantiation.
|
47
|
+
def self::inherited( subclass )
|
48
|
+
super
|
49
|
+
subclass.public_class_method( :new )
|
50
|
+
if (( type_name = subclass.name&.sub( /.*::/, '' )&.downcase ))
|
51
|
+
Pushdown::State.register_transition( type_name )
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
### Create a new Transition with the given +name+. If +data+ is specified, it will be passed
|
57
|
+
### through the transition callbacks on State (State#on_start, State#on_stop, State#on_pause,
|
58
|
+
### State#on_resume) as it is applied.
|
59
|
+
def initialize( name, data=nil )
|
60
|
+
@name = name
|
61
|
+
@data = data
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
######
|
66
|
+
public
|
67
|
+
######
|
68
|
+
|
69
|
+
##
|
70
|
+
# The name of the transition; mostly for human consumption
|
71
|
+
attr_reader :name
|
72
|
+
|
73
|
+
##
|
74
|
+
# Data to pass to the transition callbacks when applying this Transition.
|
75
|
+
attr_accessor :data
|
76
|
+
|
77
|
+
|
78
|
+
### Return a state +stack+ after the transition has been applied.
|
79
|
+
def apply( stack )
|
80
|
+
raise NotImplementedError, "%p doesn't implement required method #%p" %
|
81
|
+
[ self.class, __method__ ]
|
82
|
+
end
|
83
|
+
|
84
|
+
end # class Pushdown::Transition
|
85
|
+
|
data/lib/pushdown.rb
CHANGED
@@ -15,17 +15,17 @@ module Pushdown
|
|
15
15
|
extend Loggability
|
16
16
|
|
17
17
|
# Package version
|
18
|
-
VERSION = '0.0
|
18
|
+
VERSION = '0.1.0'
|
19
19
|
|
20
20
|
|
21
21
|
# Loggability API -- create a logger for Pushdown classes and modules
|
22
22
|
log_as :pushdown
|
23
23
|
|
24
|
-
|
25
|
-
autoload :Automaton, 'pushdown/automaton'
|
26
|
-
autoload :State, 'pushdown/state'
|
27
24
|
autoload :Transition, 'pushdown/transition'
|
28
|
-
|
25
|
+
autoload :State, 'pushdown/state'
|
26
|
+
autoload :Automaton, 'pushdown/automaton'
|
29
27
|
|
30
28
|
end # module Pushdown
|
31
29
|
|
30
|
+
Pushdown::Transition.plugin_exclusions( '**/spec/pushdown/transition/**' )
|
31
|
+
Pushdown::Transition.load_all
|
@@ -0,0 +1,151 @@
|
|
1
|
+
#!/usr/bin/env rspec -cfd
|
2
|
+
|
3
|
+
require_relative '../spec_helper'
|
4
|
+
|
5
|
+
require 'pushdown/automaton'
|
6
|
+
|
7
|
+
|
8
|
+
RSpec.describe( Pushdown::Automaton ) do
|
9
|
+
|
10
|
+
let( :extended_class ) do
|
11
|
+
the_class = Class.new
|
12
|
+
the_class.extend( described_class )
|
13
|
+
the_class
|
14
|
+
end
|
15
|
+
|
16
|
+
let( :starting_state ) do
|
17
|
+
Class.new( Pushdown::State ) do
|
18
|
+
transition_push :run, :running
|
19
|
+
def on_event( event, * )
|
20
|
+
return :run if event == :run
|
21
|
+
return nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
let( :off_state ) do
|
26
|
+
Class.new( Pushdown::State ) do
|
27
|
+
transition_push :start, :starting
|
28
|
+
def update( * )
|
29
|
+
return :start
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
let( :running_state ) { Class.new(Pushdown::State) }
|
34
|
+
|
35
|
+
let( :state_class_registry ) {{
|
36
|
+
starting: starting_state,
|
37
|
+
off: off_state,
|
38
|
+
running: running_state,
|
39
|
+
}}
|
40
|
+
|
41
|
+
|
42
|
+
it "allows a state attribute to be declared" do
|
43
|
+
extended_class.pushdown_state( :state, initial_state: :idle )
|
44
|
+
extended_class.const_set( :Idle, starting_state )
|
45
|
+
|
46
|
+
instance = extended_class.new
|
47
|
+
|
48
|
+
expect( instance.state ).to be_a( starting_state )
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
it "allows a state class registry to be inferred" do
|
53
|
+
extended_class.pushdown_state( :state, initial_state: :starting )
|
54
|
+
extended_class.const_set( :Starting, starting_state )
|
55
|
+
|
56
|
+
expect( extended_class.initial_state ).to be( starting_state )
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
it "allows a state registry to be passed as a Hash-alike" do
|
61
|
+
extended_class.pushdown_state( :status, initial_state: :starting, states: state_class_registry )
|
62
|
+
|
63
|
+
expect( extended_class.initial_status ).to be( starting_state )
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
context "for each pushdown state" do
|
68
|
+
|
69
|
+
it "allows a state registry to be passed as a (pluggable) Pushdown::State subclass" do
|
70
|
+
state_baseclass = Class.new( Pushdown::State ) do
|
71
|
+
singleton_class.attr_accessor :subclasses
|
72
|
+
def self::get_subclass( classname )
|
73
|
+
return subclasses[ classname ]
|
74
|
+
end
|
75
|
+
def self::create( class_name, *args )
|
76
|
+
return self.get_subclass( class_name ).new( *args )
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
state_baseclass.subclasses = state_class_registry
|
81
|
+
extended_class.pushdown_state( :state, initial_state: :starting, states: state_baseclass )
|
82
|
+
|
83
|
+
expect( extended_class.initial_state ).to be( starting_state )
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
it "generates an event method that applies transitions returned from #on_event" do
|
88
|
+
extended_class.pushdown_state( :state, initial_state: :starting )
|
89
|
+
extended_class.const_set( :Starting, starting_state )
|
90
|
+
extended_class.const_set( :Off, off_state )
|
91
|
+
extended_class.const_set( :Running, running_state )
|
92
|
+
|
93
|
+
instance = extended_class.new
|
94
|
+
result = instance.handle_state_event( :run )
|
95
|
+
|
96
|
+
expect( result ).to be_a( Pushdown::Transition::Push )
|
97
|
+
expect( instance.state ).to be_a( running_state )
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
it "generates an periodic update method that applies transitions returned from #update on the current state" do
|
102
|
+
extended_class.pushdown_state( :status, initial_state: :off )
|
103
|
+
extended_class.const_set( :Starting, starting_state )
|
104
|
+
extended_class.const_set( :Off, off_state )
|
105
|
+
extended_class.const_set( :Running, running_state )
|
106
|
+
|
107
|
+
instance = extended_class.new
|
108
|
+
result = instance.update_status
|
109
|
+
|
110
|
+
expect( result ).to be_a( Pushdown::Transition::Push )
|
111
|
+
expect( instance.status ).to be_a( starting_state )
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
it "generates an periodic update method that is called on every state in the stack" do
|
116
|
+
extended_class.pushdown_state( :status, initial_state: :off )
|
117
|
+
extended_class.const_set( :Starting, starting_state )
|
118
|
+
extended_class.const_set( :Off, off_state )
|
119
|
+
extended_class.const_set( :Running, running_state )
|
120
|
+
|
121
|
+
instance = extended_class.new
|
122
|
+
result = instance.shadow_update_status
|
123
|
+
|
124
|
+
expect( result ).to be_nil
|
125
|
+
expect( instance.status ).to be_a( off_state )
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
it "fetches initial state data if the extended class defines a method for doing so" do
|
130
|
+
extended_class.pushdown_state( :state, initial_state: :starting )
|
131
|
+
extended_class.const_set( :Starting, starting_state )
|
132
|
+
extended_class.const_set( :Off, off_state )
|
133
|
+
extended_class.const_set( :Running, running_state )
|
134
|
+
extended_class.attr_accessor :state_data
|
135
|
+
extended_class.define_method( :initial_state_data ) do
|
136
|
+
return self.state_data ||= {}
|
137
|
+
end
|
138
|
+
|
139
|
+
starting_state.define_method( :on_start ) do |data|
|
140
|
+
data[:starting_started] = true
|
141
|
+
end
|
142
|
+
|
143
|
+
instance = extended_class.new
|
144
|
+
|
145
|
+
expect( instance.state_data ).to eq({ starting_started: true })
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
|
@@ -0,0 +1,88 @@
|
|
1
|
+
#!/usr/bin/env rspec -cfd
|
2
|
+
|
3
|
+
require_relative '../spec_helper'
|
4
|
+
|
5
|
+
# Let autoloads decide the order
|
6
|
+
require 'pushdown'
|
7
|
+
|
8
|
+
|
9
|
+
RSpec.describe( Pushdown::State ) do
|
10
|
+
|
11
|
+
let( :state_data ) { {} }
|
12
|
+
let( :subclass ) do
|
13
|
+
Class.new( described_class )
|
14
|
+
end
|
15
|
+
|
16
|
+
let( :starting_state_class ) do
|
17
|
+
Class.new( described_class )
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
it "is an abstract class" do
|
22
|
+
expect { described_class.new }.to raise_error( NoMethodError, /\bnew\b/ )
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
describe "event handlers" do
|
27
|
+
|
28
|
+
it "has a default (no-op) callback for when it is added to the stack" do
|
29
|
+
instance = subclass.new
|
30
|
+
expect( instance.on_start(state_data) ).to be_nil
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
it "has a default (no-op) callback for when it is removed from the stack" do
|
35
|
+
instance = subclass.new
|
36
|
+
expect( instance.on_stop(state_data) ).to be_nil
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
it "has a default (no-op) callback for when it is pushed down on the stack" do
|
41
|
+
instance = subclass.new
|
42
|
+
expect( instance.on_pause(state_data) ).to be_nil
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
it "has a default (no-op) callback for when the stack is popped and it becomes current again" do
|
47
|
+
instance = subclass.new
|
48
|
+
expect( instance.on_resume(state_data) ).to be_nil
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
describe "update handlers" do
|
55
|
+
|
56
|
+
it "has a default (no-op) interval callback for when it is on the stack" do
|
57
|
+
instance = subclass.new
|
58
|
+
expect( instance.shadow_update(state_data) ).to be_nil
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
it "has a default (no-op) interval callback for when it is current" do
|
63
|
+
instance = subclass.new
|
64
|
+
expect( instance.update(state_data) ).to be_nil
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
|
71
|
+
describe "transition declaration" do
|
72
|
+
|
73
|
+
it "can declare a push transition" do
|
74
|
+
subclass.transition_push( :start, :starting )
|
75
|
+
expect( subclass.transitions[:start] ).to eq([ :push, :starting ])
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
it "can declare a pop transition" do
|
80
|
+
subclass.transition_pop( :undo )
|
81
|
+
expect( subclass.transitions[:undo] ).to eq([ :pop ])
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
end
|
88
|
+
|