M README.md +8 -7
@@ 31,18 31,19 @@ It's still mostly experimental.
$ gem install pushdown
-## Contributing
+## Development
-You can check out the current development source with Mercurial via its
-[project page](http://bitbucket.org/ged/pushdown). Or if you prefer
-Git, via [its Github mirror](https://github.com/ged/pushdown).
+You can check out the current source with Git via Gitlab:
+
+ $ hg clone http://hg.sr.ht/~ged/Pushdown
+ $ cd Pushdown
After checking out the source, run:
- $ rake newb
+ $ gem install -Ng
+ $ rake setup
-This task will install any missing dependencies, run the tests/specs,
-and generate the API documentation.
+This task will install dependencies, and do any other necessary setup for development.
## Author(s)
M lib/pushdown/state.rb +23 -5
@@ 57,31 57,46 @@ class Pushdown::State
end
+ ### Set up new States with an optional +data+ object.
+ def initialize( data=nil )
+ @data = data
+ end
+
+
+ ######
+ public
+ ######
+
+ ##
+ # The state data object that was used to create the State (if any)
+ attr_reader :data
+
+
#
# Stack callbacks
#
### Stack callback -- called when the state is added to the stack.
- def on_start( data=nil )
+ def on_start
return nil # no-op
end
### Stack callback -- called when the state is removed from the stack.
- def on_stop( data=nil )
+ def on_stop
return nil # no-op
end
### Stack callback -- called when another state is pushed over this one.
- def on_pause( data=nil )
+ def on_pause
return nil # no-op
end
### Stack callback -- called when another state is popped off from in front of
### this one, making it the current state.
- def on_resume( data=nil )
+ def on_resume
return nil # no-op
end
@@ 146,7 161,10 @@ class Pushdown::State
if state_class_name
state_class = automaton.class.pushdown_state_class( stack_name, state_class_name )
- return Pushdown::Transition.create( transition_type, transition_name, state_class )
+ state_data = self.data
+
+ return Pushdown::Transition.
+ create( transition_type, transition_name, state_class, state_data )
else
return Pushdown::Transition.create( transition_type, transition_name )
end
M lib/pushdown/transition.rb +2 -9
@@ 35,12 35,9 @@ class Pushdown::Transition
end
- ### Create a new Transition with the given +name+. If +data+ is specified, it will be passed
- ### through the transition callbacks on State (State#on_start, State#on_stop, State#on_pause,
- ### State#on_resume) as it is applied.
- def initialize( name, data=nil )
+ ### Create a new Transition with the given +name+.
+ def initialize( name )
@name = name
- @data = data
end
@@ 52,10 49,6 @@ class Pushdown::Transition
# The name of the transition; mostly for human consumption
attr_reader :name
- ##
- # Data to pass to the transition callbacks when applying this Transition.
- attr_accessor :data
-
### Return a state +stack+ after the transition has been applied.
def apply( stack )
M lib/pushdown/transition/pop.rb +2 -2
@@ 26,8 26,8 @@ class Pushdown::Transition::Pop < Pushdo
self.log.debug "popping a state"
@popped_state = stack.pop
- self.data = @popped_state.on_stop( self.data )
- stack.last.on_resume( self.data )
+ @popped_state.on_stop
+ stack.last.on_resume
return stack
end
M lib/pushdown/transition/push.rb +11 -5
@@ 11,9 11,11 @@ class Pushdown::Transition::Push < Pushd
### Create a transition that will Push an instance of the given +state_class+ to
### the stack.
- def initialize( name, state_class, *args )
- super( name, *args )
+ def initialize( name, state_class, data=nil )
+ super( name )
+
@state_class = state_class
+ @data = data
end
@@ 25,15 27,19 @@ class Pushdown::Transition::Push < Pushd
# The State to push to.
attr_reader :state_class
+ ##
+ # The data object to pass to the #state_class's constructor
+ attr_reader :data
+
### Apply the transition to the given +stack+.
def apply( stack )
- state = self.state_class.new
+ state = self.state_class.new( self.data )
self.log.debug "pushing a new state: %p" % [ state ]
- self.data = stack.last.on_pause( self.data ) if stack.last
+ stack.last.on_pause if stack.last
stack.push( state )
- state.on_start( self.data )
+ state.on_start
return stack
end
M lib/pushdown/transition/replace.rb +11 -5
@@ 10,9 10,11 @@ class Pushdown::Transition::Replace < Pu
### Create a transition that will Replace all the states on the current stack
### with an instance of the given +state_class+.
- def initialize( name, state_class, *args )
- super( name, *args )
+ def initialize( name, state_class, data=nil )
+ super( name )
+
@state_class = state_class
+ @data = data
end
@@ 24,18 26,22 @@ class Pushdown::Transition::Replace < Pu
# The State to replace the stack members with.
attr_reader :state_class
+ ##
+ # The data object to pass to the #state_class's constructor
+ attr_reader :data
+
### Apply the transition to the given +stack+.
def apply( stack )
- state = self.state_class.new
+ state = self.state_class.new( self.data )
self.log.debug "replacing current state with a new state: %p" % [ state ]
while ( old_state = stack.pop )
- self.data = old_state.on_stop( self.data )
+ old_state.on_stop
end
stack.push( state )
- state.on_start( self.data )
+ state.on_start
return stack
end
M lib/pushdown/transition/switch.rb +14 -5
@@ 10,9 10,11 @@ class Pushdown::Transition::Switch < Pus
### Create a transition that will Switch the current State with an instance of
### the given +state_class+ on the stack.
- def initialize( name, state_class, *args )
- super( name, *args )
+ def initialize( name, state_class, data=nil )
+ super( name )
+
@state_class = state_class
+ @data = data
end
@@ 24,16 26,23 @@ class Pushdown::Transition::Switch < Pus
# The State to push to.
attr_reader :state_class
+ ##
+ # The data object to pass to the #state_class's constructor
+ attr_reader :data
+
### Apply the transition to the given +stack+.
def apply( stack )
- state = self.state_class.new
+ raise Pushdown::TransitionError, "can't switch on an empty stack" if stack.empty?
+
+ state = self.state_class.new( self.data )
self.log.debug "switching current state with a new state: %p" % [ state ]
old_state = stack.pop
- self.data = old_state.on_stop( self.data )
+ old_state.on_stop if old_state
+
stack.push( state )
- state.on_start( self.data )
+ state.on_start
return stack
end
M pushdown.gemspec +4 -4
@@ 1,18 1,18 @@
# -*- encoding: utf-8 -*-
-# stub: pushdown 0.2.0.pre.20210830140713 ruby lib
+# stub: pushdown 0.3.0.pre.20210901151908 ruby lib
Gem::Specification.new do |s|
s.name = "pushdown".freeze
- s.version = "0.2.0.pre.20210830140713"
+ s.version = "0.3.0.pre.20210901151908"
s.required_rubygems_version = Gem::Requirement.new("> 1.3.1".freeze) if s.respond_to? :required_rubygems_version=
s.metadata = { "bug_tracker_uri" => "http://todo.sr.ht/~ged/Pushdown", "changelog_uri" => "http://deveiate.org/code/pushdown/History_md.html", "documentation_uri" => "http://deveiate.org/code/pushdown", "homepage_uri" => "http://hg.sr.ht/~ged/Pushdown", "source_uri" => "http://hg.sr.ht/~ged/Pushdown" } if s.respond_to? :metadata=
s.require_paths = ["lib".freeze]
s.authors = ["Michael Granger".freeze]
- s.date = "2021-08-30"
+ s.date = "2021-09-01"
s.description = "A pushdown automaton toolkit for Ruby. It's based on the State Manager from the Amethyst project.".freeze
s.email = ["ged@faeriemud.org".freeze]
- s.files = ["History.md".freeze, "LICENSE.txt".freeze, "README.md".freeze, "lib/pushdown.rb".freeze, "lib/pushdown/automaton.rb".freeze, "lib/pushdown/exceptions.rb".freeze, "lib/pushdown/state.rb".freeze, "lib/pushdown/transition.rb".freeze, "lib/pushdown/transition/pop.rb".freeze, "lib/pushdown/transition/push.rb".freeze, "lib/pushdown/transition/replace.rb".freeze, "lib/pushdown/transition/switch.rb".freeze, "spec/pushdown/automaton_spec.rb".freeze, "spec/pushdown/state_spec.rb".freeze, "spec/pushdown/transition/pop_spec.rb".freeze, "spec/pushdown/transition/push_spec.rb".freeze, "spec/pushdown/transition/replace_spec.rb".freeze, "spec/pushdown/transition/switch_spec.rb".freeze, "spec/pushdown/transition_spec.rb".freeze, "spec/pushdown_spec.rb".freeze, "spec/spec_helper.rb".freeze]
+ s.files = ["History.md".freeze, "LICENSE.txt".freeze, "README.md".freeze, "lib/pushdown.rb".freeze, "lib/pushdown/automaton.rb".freeze, "lib/pushdown/exceptions.rb".freeze, "lib/pushdown/spec_helpers.rb".freeze, "lib/pushdown/state.rb".freeze, "lib/pushdown/transition.rb".freeze, "lib/pushdown/transition/pop.rb".freeze, "lib/pushdown/transition/push.rb".freeze, "lib/pushdown/transition/replace.rb".freeze, "lib/pushdown/transition/switch.rb".freeze, "spec/pushdown/automaton_spec.rb".freeze, "spec/pushdown/spec_helpers_spec.rb".freeze, "spec/pushdown/state_spec.rb".freeze, "spec/pushdown/transition/pop_spec.rb".freeze, "spec/pushdown/transition/push_spec.rb".freeze, "spec/pushdown/transition/replace_spec.rb".freeze, "spec/pushdown/transition/switch_spec.rb".freeze, "spec/pushdown/transition_spec.rb".freeze, "spec/pushdown_spec.rb".freeze, "spec/spec_helper.rb".freeze]
s.homepage = "http://hg.sr.ht/~ged/Pushdown".freeze
s.licenses = ["BSD-3-Clause".freeze]
s.rubygems_version = "3.1.6".freeze
M spec/pushdown/automaton_spec.rb +1 -1
@@ 137,7 137,7 @@ RSpec.describe( Pushdown::Automaton ) do
return self.state_data ||= {}
end
- starting_state.define_method( :on_start ) do |data|
+ starting_state.define_method( :on_start ) do
data[:starting_started] = true
end
M spec/pushdown/state_spec.rb +18 -6
@@ 55,25 55,25 @@ RSpec.describe( Pushdown::State ) do
it "has a default (no-op) callback for when it is added to the stack" do
instance = subclass.new
- expect( instance.on_start(state_data) ).to be_nil
+ expect( instance.on_start ).to be_nil
end
it "has a default (no-op) callback for when it is removed from the stack" do
instance = subclass.new
- expect( instance.on_stop(state_data) ).to be_nil
+ expect( instance.on_stop ).to be_nil
end
it "has a default (no-op) callback for when it is pushed down on the stack" do
instance = subclass.new
- expect( instance.on_pause(state_data) ).to be_nil
+ expect( instance.on_pause ).to be_nil
end
it "has a default (no-op) callback for when the stack is popped and it becomes current again" do
instance = subclass.new
- expect( instance.on_resume(state_data) ).to be_nil
+ expect( instance.on_resume ).to be_nil
end
end
@@ 93,13 93,13 @@ RSpec.describe( Pushdown::State ) do
it "has a default (no-op) interval callback for when it is current" do
instance = subclass.new
- expect( instance.update(state_data) ).to be_nil
+ expect( instance.update ).to be_nil
end
it "has a default (no-op) interval callback for when it is on the stack" do
instance = subclass.new
- expect( instance.shadow_update(state_data) ).to be_nil
+ expect( instance.shadow_update ).to be_nil
end
end
@@ 136,6 136,18 @@ RSpec.describe( Pushdown::State ) do
expect( result.data ).to be_nil
end
+
+ it "can create a transition it has declared that doesn't take a state class" do
+ subclass.transition_pop( :quit )
+ instance = subclass.new
+
+ automaton = automaton_class.new
+
+ result = instance.transition( :quit, automaton, :state )
+ expect( result ).to be_a( Pushdown::Transition::Pop )
+ expect( result.name ).to eq( :quit )
+ end
+
end
end
M spec/pushdown/transition/pop_spec.rb +3 -6
@@ 17,8 17,6 @@ RSpec.describe( Pushdown::Transition::Po
let( :state_b ) { state_class.new }
let( :state_c ) { state_class.new }
- let( :state_data ) { Object.new }
-
let( :stack ) do
return [ state_a, state_b, state_c ]
end
@@ 35,11 33,10 @@ RSpec.describe( Pushdown::Transition::Po
it "passes state data through the transition callbacks" do
- transition = described_class.new( :pop_test, state_data )
+ transition = described_class.new( :pop_test )
- expect( state_c ).to receive( :on_stop ).with( state_data ).
- and_return( state_data ).once.ordered
- expect( state_b ).to receive( :on_resume ).with( state_data ).once.ordered
+ expect( state_c ).to receive( :on_stop ).with( no_args ).once.ordered
+ expect( state_b ).to receive( :on_resume ).with( no_args ).once.ordered
transition.apply( stack )
end
M spec/pushdown/transition/push_spec.rb +22 -14
@@ 16,18 16,12 @@ RSpec.describe( Pushdown::Transition::Pu
Class.new( Pushdown::State )
end
- let( :state_a ) { state_class.new }
- let( :state_b ) { state_class.new }
- let( :state_c ) { other_state_class.new }
-
- let( :stack ) do
- return [ state_a, state_b ]
- end
-
let( :state_data ) { Object.new }
+
it "pushes a new state onto the stack when applied" do
+ stack = [ state_class.new, state_class.new ]
transition = described_class.new( :push_test, other_state_class )
new_stack = transition.apply( stack )
@@ 35,17 29,31 @@ RSpec.describe( Pushdown::Transition::Pu
expect( new_stack ).to be_an( Array )
expect( new_stack.length ).to eq( 3 )
expect( new_stack.last ).to be_a( other_state_class )
+ expect( new_stack.last.data ).to be_nil
end
- it "passes state data through the transition callbacks" do
- transition = described_class.new( :push_test, other_state_class, state_data )
+ it "passes on state data to the new state if given" do
+ stack = []
+ transition = described_class.new( :push_test, state_class, state_data )
+
+ new_stack = transition.apply( stack )
+
+ expect( new_stack.last.data ).to be( state_data )
+ end
+
- expect( state_b ).to receive( :on_pause ).
- with( state_data ).once.and_return( state_data ).ordered
+ it "calls the transition callbacks of the former current state and the new state" do
+ new_state = instance_double( other_state_class )
+ stack = [
+ state_class.new,
+ state_class.new
+ ]
+ transition = described_class.new( :push_test, other_state_class )
- expect( other_state_class ).to receive( :new ).and_return( state_c )
- expect( state_c ).to receive( :on_start ).with( state_data ).once.ordered
+ expect( stack.last ).to receive( :on_pause )
+ expect( other_state_class ).to receive( :new ).and_return( new_state )
+ expect( new_state ).to receive( :on_start )
transition.apply( stack )
end
M spec/pushdown/transition/replace_spec.rb +24 -10
@@ 28,6 28,7 @@ RSpec.describe( Pushdown::Transition::Re
it "replaces the current stack members with a single new instance of a state when applied" do
+ stack = [ state_class.new, state_class.new ]
transition = described_class.new( :replace_test, other_state_class )
new_stack = transition.apply( stack )
@@ 38,18 39,31 @@ RSpec.describe( Pushdown::Transition::Re
end
- it "passes state data through the transition callbacks" do
- transition = described_class.new( :replace_test, other_state_class, state_data )
+ it "passes on state data to the new state if given" do
+ stack = []
+ transition = described_class.new( :replace_test, state_class, state_data )
+
+ new_stack = transition.apply( stack )
+
+ expect( new_stack.last.data ).to be( state_data )
+ end
+
- expect( state_c ).to receive( :on_stop ).
- with( state_data ).once.and_return( state_data ).ordered
- expect( state_b ).to receive( :on_stop ).
- with( state_data ).once.and_return( state_data ).ordered
- expect( state_a ).to receive( :on_stop ).
- with( state_data ).once.and_return( state_data ).ordered
+ it "calls the transition callbacks of all the former states and the new state" do
+ new_state = instance_double( other_state_class )
+ stack = [
+ state_class.new,
+ state_class.new,
+ other_state_class.new
+ ]
+ transition = described_class.new( :replace_test, other_state_class )
- expect( other_state_class ).to receive( :new ).and_return( state_c )
- expect( state_c ).to receive( :on_start ).with( state_data ).once.ordered
+ expect( stack[2] ).to receive( :on_stop ).ordered
+ expect( stack[1] ).to receive( :on_stop ).ordered
+ expect( stack[0] ).to receive( :on_stop ).ordered
+
+ expect( other_state_class ).to receive( :new ).and_return( new_state )
+ expect( new_state ).to receive( :on_start ).ordered
transition.apply( stack )
end
M spec/pushdown/transition/switch_spec.rb +33 -15
@@ 15,18 15,12 @@ RSpec.describe( Pushdown::Transition::Sw
Class.new( Pushdown::State )
end
- let( :state_a ) { state_class.new }
- let( :state_b ) { state_class.new }
- let( :state_c ) { other_state_class.new }
-
- let( :stack ) do
- return [ state_a, state_b ]
- end
-
let( :state_data ) { Object.new }
- it "pops the current state off the stack and adds a new state when applied" do
+
+ it "switches the current state the stack with a new one when applied" do
+ stack = [ state_class.new, state_class.new ]
transition = described_class.new( :switch_test, other_state_class )
new_stack = transition.apply( stack )
@@ 34,20 28,44 @@ RSpec.describe( Pushdown::Transition::Sw
expect( new_stack ).to be_an( Array )
expect( new_stack.length ).to eq( 2 )
expect( new_stack.last ).to be_a( other_state_class )
+ expect( new_stack.last.data ).to be_nil
end
- it "passes state data through the transition callbacks" do
- transition = described_class.new( :switch_test, other_state_class, state_data )
+ it "passes on state data to the new state if given" do
+ stack = [ state_class.new ]
+ transition = described_class.new( :switch_test, state_class, state_data )
+
+ new_stack = transition.apply( stack )
+
+ expect( new_stack.last.data ).to be( state_data )
+ end
+
- expect( state_b ).to receive( :on_stop ).
- with( state_data ).once.and_return( state_data ).ordered
+ it "calls the transition callbacks of the former current state and the new state" do
+ new_state = instance_double( other_state_class )
+ stack = [
+ state_class.new,
+ state_class.new
+ ]
+ transition = described_class.new( :switch_test, other_state_class )
- expect( other_state_class ).to receive( :new ).and_return( state_c )
- expect( state_c ).to receive( :on_start ).with( state_data ).once.ordered
+ expect( stack.last ).to receive( :on_stop )
+ expect( other_state_class ).to receive( :new ).and_return( new_state )
+ expect( new_state ).to receive( :on_start )
transition.apply( stack )
end
+
+ it "errors if applied to an empty stack" do
+ stack = []
+ transition = described_class.new( :switch_test, other_state_class )
+
+ expect {
+ transition.apply( stack )
+ }.to raise_error( Pushdown::TransitionError, /can't switch/i )
+ end
+
end