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.
@@ -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.1'
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
+