pushdown 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf2b58a9924b03dc5e15d6c0fca42ebac8988fcdbf9e0b02f1ab3b1e2365de56
4
- data.tar.gz: 7117970f25e2e310be68a2269dd77216cae26442d298e73dd1bb580a29752914
3
+ metadata.gz: de5a87ff7d9744a6c71e977a4d5e2fbe03be08efc393d5fc8c84b7a9f4f720c2
4
+ data.tar.gz: 6a94eca59001d1640a5676e9a135b56690c6e20f2ec75e49740b68b036005870
5
5
  SHA512:
6
- metadata.gz: 9b41ba0c24de3f75fb91fbc02e3b20afc5d2d4a8aac8f43d9ae4237e354fcab2611d48eb885f2bef542d9d9da5bbb5c6686f98c78ea26287e84e9c7fb8242011
7
- data.tar.gz: 1d84bef861cf31696148031a01a13fd04ef5a76d9bd45733f3a0043976e62a97aa56836b3c7bdcaf54d45bf798df9abe4c59b619ebd406f7d457442340d1ed5c
6
+ metadata.gz: 677caf47c2cebd2ac9023a72d16e99881b97be5cacd439aae7f472bdefb1d1e7d7ee75151211988f1b354bc1f9339d3ab4483dce83cec24740579562e48ee41c
7
+ data.tar.gz: e32e468403256a51c4683bff1bbf5f332461ed5fc22c43369509b8e241e4f0a102ba105cbfa925474b55e4420dfc8e3ff49af42a8abdd793b7e046e2f1810f69
checksums.yaml.gz.sig CHANGED
Binary file
data/History.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ---
4
4
 
5
+ ## v0.2.0 [2021-08-31] Michael Granger <ged@faeriemud.org>
6
+
7
+ Improvements:
8
+
9
+ - Move responsibility for creating transitions into State.
10
+ - Add spec helpers with a transition matcher.
11
+ - Add missing event handler method to State
12
+
5
13
 
6
14
  ## v0.1.0 [2021-08-27] Michael Granger <ged@faeriemud.org>
7
15
 
data/README.md CHANGED
@@ -49,9 +49,42 @@ and generate the API documentation.
49
49
 
50
50
  * Michael Granger <ged@faeriemud.org>
51
51
 
52
+ While Pushdown does not contain any [Amethyst Game Engine][amethyst] source code, it does borrow heavily from its ideas and nomenclature. Many thanks to the Amethyst team for the inspiration.
53
+
54
+ Thanks also to Alyssa Verkade for the initial idea.
55
+
52
56
 
53
57
  ## License
54
58
 
59
+ This gem includes code from the rspec-expectations gem, used under the
60
+ terms of the MIT License:
61
+
62
+ Copyright (c) 2012 David Chelimsky, Myron Marston
63
+ Copyright (c) 2006 David Chelimsky, The RSpec Development Team
64
+ Copyright (c) 2005 Steven Baker
65
+
66
+ Permission is hereby granted, free of charge, to any person obtaining
67
+ a copy of this software and associated documentation files (the
68
+ "Software"), to deal in the Software without restriction, including
69
+ without limitation the rights to use, copy, modify, merge, publish,
70
+ distribute, sublicense, and/or sell copies of the Software, and to
71
+ permit persons to whom the Software is furnished to do so, subject to
72
+ the following conditions:
73
+
74
+ The above copyright notice and this permission notice shall be
75
+ included in all copies or substantial portions of the Software.
76
+
77
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
78
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
79
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
80
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
81
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
82
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
83
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
84
+
85
+
86
+ Pushdown itself is:
87
+
55
88
  Copyright (c) 2019-2021, Michael Granger
56
89
  All rights reserved.
57
90
 
@@ -80,6 +113,6 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
80
113
  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
81
114
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
82
115
 
83
-
116
+ [amethyst]: https://amethyst.rs/
84
117
  [amethyst-state-manager]: https://book.amethyst.rs/stable/concepts/state.html#state-manager
85
118
 
@@ -29,6 +29,57 @@ module Pushdown::Automaton
29
29
  end
30
30
 
31
31
 
32
+ # A mixin to add instance methods for setting up pushdown states.
33
+ module InstanceMethods
34
+
35
+ ### Overload the initializer to push the initial state object for any pushdown
36
+ ### states.
37
+ def initialize( * )
38
+ super if defined?( super )
39
+
40
+ self.class.pushdown_states.each do |name, config|
41
+ self.log.debug "Pushing initial %s" % [ name ]
42
+
43
+ state_class = self.class.public_send( "initial_#{name}" ) or
44
+ raise "unset initial_%s while pushing initial state" % [ name ]
45
+
46
+ data = if self.respond_to?( "initial_#{name}_data" )
47
+ self.public_send( "initial_#{name}_data" )
48
+ else
49
+ nil
50
+ end
51
+
52
+ self.log.info "Pushing an instance of %p as the initial state." % [ state_class ]
53
+ transition = Pushdown::Transition.create( :push, :initial, state_class, data )
54
+ self.log.debug " applying %p to an new empty stack" % [ transition ]
55
+ stack = transition.apply( [] )
56
+ self.instance_variable_set( "@#{name}_stack", stack )
57
+ end
58
+ end
59
+
60
+
61
+ ### The body of the event handler, called by the #handle_{name}_event.
62
+ def handle_pushdown_result( stack, result, name )
63
+ if result.is_a?( Symbol )
64
+ current_state = stack.last
65
+ result = current_state.transition( result, self, name )
66
+ end
67
+
68
+ if result.is_a?( Pushdown::Transition )
69
+ new_stack = result.apply( stack )
70
+ stack.replace( new_stack )
71
+ end
72
+
73
+ return result
74
+ end
75
+
76
+
77
+ # :TODO: Methods that call #update, #on_event, etc. and then manage the
78
+ # application of any transition(s) that are returned.
79
+
80
+ end # module InstanceMethods
81
+
82
+
32
83
  ### Generate the pushdown API methods for the pushdown automaton with the given
33
84
  ### +name+ and install them in the extending +object+.
34
85
  def self::install_state_methods( name, object )
@@ -127,65 +178,6 @@ module Pushdown::Automaton
127
178
  end
128
179
 
129
180
 
130
- # A mixin to add instance methods for setting up pushdown states.
131
- module InstanceMethods
132
-
133
- ### Overload the initializer to push the initial state object for any pushdown
134
- ### states.
135
- def initialize( * )
136
- super if defined?( super )
137
-
138
- self.class.pushdown_states.each do |name, config|
139
- self.log.debug "Pushing initial %s" % [ name ]
140
-
141
- state_class = self.class.public_send( "initial_#{name}" ) or
142
- raise "unset initial_%s while pushing initial state" % [ name ]
143
-
144
- data = if self.respond_to?( "initial_#{name}_data" )
145
- self.public_send( "initial_#{name}_data" )
146
- else
147
- nil
148
- end
149
-
150
- transition = Pushdown::Transition.create( :push, :initial, state_class, data )
151
- stack = transition.apply( [] )
152
- self.instance_variable_set( "@#{name}_stack", stack )
153
- end
154
- end
155
-
156
-
157
- ### The body of the event handler, called by the #handle_{name}_event.
158
- def handle_pushdown_result( stack, result, name )
159
- if result.is_a?( Symbol )
160
- transition_name = result
161
- current_state = stack.last
162
- self.log.debug "Looking up the %p transition for %p" % [ result, current_state ]
163
- transition_type, state_class_name = current_state.class.transitions[ transition_name ]
164
- raise "no such transition %p for %s" % [ transition_name, name ] unless transition_type
165
-
166
- result = if state_class_name
167
- state_class = self.class.pushdown_state_class( name, state_class_name )
168
- Pushdown::Transition.create( transition_type, transition_name, state_class )
169
- else
170
- Pushdown::Transition.create( transition_type, transition_name )
171
- end
172
- end
173
-
174
- if result.is_a?( Pushdown::Transition )
175
- new_stack = result.apply( stack )
176
- stack.replace( new_stack )
177
- end
178
-
179
- return result
180
- end
181
-
182
-
183
- # :TODO: Methods that call #update, #on_event, etc. and then manage the
184
- # application of any transition(s) that are returned.
185
-
186
- end # module InstanceMethods
187
-
188
-
189
181
  ### Declare a attribute +name+ which is a pushdown state.
190
182
  def pushdown_state( name, initial_state:, states: nil )
191
183
  @pushdown_states[ name ] = { initial_state: initial_state, states: states }
@@ -0,0 +1,263 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'rspec'
5
+ require 'rspec/matchers'
6
+
7
+ require 'loggability'
8
+
9
+ require 'pushdown' unless defined?( Pushdown )
10
+
11
+
12
+ module Pushdown::SpecHelpers
13
+
14
+
15
+ # RSpec matcher for matching transitions of Pushdown::States
16
+ class StateTransitionMatcher
17
+ extend Loggability
18
+ include RSpec::Matchers
19
+
20
+ DEFAULT_CALLBACK = [ :update ]
21
+
22
+ log_to :pushdown
23
+
24
+
25
+ ### Create a new matcher that expects a transition to occur.
26
+ def initialize
27
+ @expected_type = nil
28
+ @target_state = nil
29
+ @callback = nil
30
+ @additional_expectations = []
31
+ @state = nil
32
+ @result = nil
33
+ @failure_description = nil
34
+ end
35
+
36
+
37
+ attr_reader :expected_type,
38
+ :target_state,
39
+ :callback,
40
+ :additional_expectations,
41
+ :state,
42
+ :result,
43
+ :failure_description
44
+
45
+
46
+ ### RSpec matcher API -- returns +true+ if all expectations are met after calling
47
+ ### #update on the specified +state+.
48
+ def matches?( state )
49
+ @state = state
50
+
51
+ return self.update_ran_without_error? &&
52
+ self.update_returned_transition? &&
53
+ self.correct_transition_type? &&
54
+ self.correct_target_state? &&
55
+ self.matches_additional_expectations?
56
+ end
57
+
58
+
59
+ ### RSpec matcher API -- return a message describing an expectation failure.
60
+ def failure_message
61
+ return "%p: %s" % [ self.state, self.describe_failure ]
62
+ end
63
+
64
+
65
+ ### RSpec matcher API -- return a message describing an expectation being met
66
+ ### when the matcher was used in a negated context.
67
+ def failure_message_when_negated
68
+ return "%p: %s" % [ self.state, self.describe_negated_failure ]
69
+ end
70
+
71
+
72
+ #
73
+ # Mutators
74
+ #
75
+
76
+ ### Add an additional expectation that the transition that occurs be of the specified
77
+ ### +transition_type+.
78
+ def via_transition_type( transition_type )
79
+ @expected_type = transition_type
80
+ return self
81
+ end
82
+ alias_method :via, :via_transition_type
83
+
84
+
85
+ ### Add an additional expectation that the state that is transitioned to be of
86
+ ### the given +state_name+. This only applies to transitions that take a target
87
+ ### state type. Expecting a particular state_name in transitions which do not take
88
+ ### a state is undefined behavior.
89
+ def to_state( state_name )
90
+ @target_state = state_name
91
+ return self
92
+ end
93
+ alias_method :to, :to_state
94
+
95
+
96
+ ### Specify that the operation that should cause the transition is the #update callback.
97
+ ### This is the default.
98
+ def on_update
99
+ raise ScriptError, "can't specify more than one callback" if self.callback
100
+ @callback = [ :update ]
101
+ return self
102
+ end
103
+
104
+
105
+ ### Specify that the operation that should cause the transition is the #on_event callback.
106
+ def on_an_event( event )
107
+ raise ScriptError, "can't specify more than one callback" if self.callback
108
+ @callback = [ :on_event, event ]
109
+ return self
110
+ end
111
+ alias_method :on_event, :on_an_event
112
+
113
+
114
+ #########
115
+ protected
116
+ #########
117
+
118
+ ### Call the state's update callback and record the result, then return +true+
119
+ ### if no exception was raised.
120
+ def update_ran_without_error?
121
+ operation = self.callback || DEFAULT_CALLBACK
122
+
123
+ @result = begin
124
+ self.state.public_send( *operation )
125
+ rescue => err
126
+ err
127
+ end
128
+
129
+ return !@result.is_a?( Exception )
130
+ end
131
+
132
+
133
+ ### Returns +true+ if the result of calling #update was a Transition or a Symbol
134
+ ### that corresponds with a valid transition.
135
+ def update_returned_transition?
136
+ case self.result
137
+ when Pushdown::Transition
138
+ return true
139
+ when Symbol
140
+ return self.state.class.transitions.include?( self.result )
141
+ else
142
+ return false
143
+ end
144
+ end
145
+
146
+
147
+ ### Returns +true+ if a transition type was specified and the transition which
148
+ ### occurred was of that type.
149
+ def correct_transition_type?
150
+ type = self.expected_type or return true
151
+
152
+ case self.result
153
+ when Pushdown::Transition
154
+ self.result.type_name == type
155
+ when Symbol
156
+ self.state.class.transitions[ self.result ].first == type
157
+ else
158
+ raise "unexpected transition result type %p" % [ self.result ]
159
+ end
160
+ end
161
+
162
+
163
+ ### Returns +true+ if a target state was specified and the transition which
164
+ ### occurred was to that state.
165
+ def correct_target_state?
166
+ state_name = self.target_state or return true
167
+
168
+ case self.result
169
+ when Pushdown::Transition
170
+ return self.result.respond_to?( :state_class ) &&
171
+ self.result.state_class.type_name == state_name
172
+ when Symbol
173
+ self.state.class.transitions[ self.result ][ 1 ] == state_name
174
+ end
175
+ end
176
+
177
+
178
+ ### Build an appropriate failure messages for the matcher.
179
+ def describe_failure
180
+ desc = String.new( "expected to transition" )
181
+ desc << " via %s" % [ self.expected_type ] if self.expected_type
182
+ desc << " to %s" % [ self.target_state ] if self.target_state
183
+
184
+ if self.callback
185
+ methname, arg = self.callback
186
+ desc << " when #%s is called" % [ methname ]
187
+ desc << " with %p" % [ arg ] if arg
188
+ end
189
+
190
+ desc << ', but '
191
+
192
+ case self.result
193
+ when Exception
194
+ err = self.result
195
+ desc << "got %p: %s" % [ err.class, err.message ]
196
+ when Symbol
197
+ transition = self.state.class.transitions[ self.result ]
198
+
199
+ if transition
200
+ desc << "it returned a %s transition " % [ transition.first ]
201
+ desc << "to %s " % [ transition[1] ] if transition[1]
202
+ desc << " instead"
203
+ else
204
+ desc << "it returned an unmapped Symbol (%p)" % [ self.result ]
205
+ end
206
+ when Pushdown::Transition
207
+ desc << "it returned a %s transition" % [ self.result.type_name ]
208
+ desc << " to %s" % [ self.result.state_class.type_name ] if
209
+ self.result.respond_to?( :state_class )
210
+ desc << " instead"
211
+ else
212
+ desc << "it did not (returned: %p)" % [ self.result ]
213
+ end
214
+
215
+ return desc
216
+ end
217
+
218
+
219
+ ### Build an appropriate failure message for the matcher.
220
+ def describe_negated_failure
221
+ desc = String.new( "expected not to transition" )
222
+ desc << " via %s" % [ self.expected_type ] if self.expected_type
223
+ desc << " to %s" % [ self.target_state ] if self.target_state
224
+
225
+ desc << ', but it did.'
226
+
227
+ return desc
228
+ end
229
+
230
+
231
+ ### Return an Array of descriptions of the members that were expected to be included in the
232
+ ### state body, if any were specified. If none were specified, returns an empty
233
+ ### Array.
234
+ def describe_additional_expectations
235
+ return self.additional_expectations.map( &:description )
236
+ end
237
+
238
+
239
+ ### Check that any additional matchers registered via the `.and` mutator also
240
+ ### match the parsed state body.
241
+ def matches_additional_expectations?
242
+ return self.additional_expectations.all? do |matcher|
243
+ matcher.matches?( self.parsed_state_body ) or
244
+ fail_with( matcher.failure_message )
245
+ end
246
+ end
247
+
248
+ end # class HaveJSONBodyMatcher
249
+
250
+
251
+ ###############
252
+ module_function
253
+ ###############
254
+
255
+ ### Set up an expectation that a transition or valid Symbol will be returned.
256
+ def transition
257
+ return StateTransitionMatcher.new
258
+ end
259
+
260
+
261
+ end # module Pushdown::SpecHelpers
262
+
263
+
@@ -43,11 +43,20 @@ class Pushdown::State
43
43
  self.transitions[ transition_name ] = [ type, *args ]
44
44
  end
45
45
 
46
- method_name = "transition_#{type}"
46
+ method_name = "transition_%s" % [ type ]
47
+ self.log.info "Setting up transition declaration method %p" % [ method_name ]
47
48
  define_singleton_method( method_name, &meth )
48
49
  end
49
50
 
50
51
 
52
+ ### Return the transition's type as a lowercase Symbol, such as that specified
53
+ ### in transition declarations.
54
+ def self::type_name
55
+ class_name = self.name or return :anonymous
56
+ return class_name.sub( /.*::/, '' ).downcase.to_sym
57
+ end
58
+
59
+
51
60
  #
52
61
  # Stack callbacks
53
62
  #
@@ -78,10 +87,22 @@ class Pushdown::State
78
87
 
79
88
 
80
89
  #
81
- # State callbacks
90
+ # Event callbacks
91
+ #
92
+
93
+ ### Event callback -- called by the automaton when its #on_<stackname>_event method
94
+ ### is called. This method can return a Transition or a Symbol which maps to one.
95
+ def on_event( event )
96
+ return nil # no-op
97
+ end
98
+
99
+
100
+ #
101
+ # Interval callbacks
82
102
  #
83
103
 
84
- ### State callback -- interval callback called when the state is the current one.
104
+ ### State callback -- interval callback called when the state is the current
105
+ ### one. This method can return a Transition or a Symbol which maps to one.
85
106
  def update( *data )
86
107
  return nil # no-op
87
108
  end
@@ -98,6 +119,13 @@ class Pushdown::State
98
119
  # Introspection/information
99
120
  #
100
121
 
122
+ ### Return the transition's type as a lowercase Symbol, such as that specified
123
+ ### in transition declarations.
124
+ def type_name
125
+ return self.class.type_name
126
+ end
127
+
128
+
101
129
  ### Return a description of the State as an engine phrase.
102
130
  def description
103
131
  return "%#x" % [ self.class.object_id ] unless self.class.name
@@ -106,5 +134,23 @@ class Pushdown::State
106
134
  gsub( /([a-z])([A-Z])/ ) { "#$1 #$2" }.downcase
107
135
  end
108
136
 
137
+
138
+ ### Create a new instance of Pushdown::Transition named +transition_name+ that
139
+ ### has been declared using one of the Transition Declaration methods.
140
+ def transition( transition_name, automaton, stack_name )
141
+ self.log.debug "Looking up the %p transition for %p via %p" %
142
+ [ transition_name, self, automaton ]
143
+
144
+ transition_type, state_class_name = self.class.transitions[ transition_name ]
145
+ raise "no such transition %p for %p" % [ transition_name, self.class ] unless transition_type
146
+
147
+ if state_class_name
148
+ state_class = automaton.class.pushdown_state_class( stack_name, state_class_name )
149
+ return Pushdown::Transition.create( transition_type, transition_name, state_class )
150
+ else
151
+ return Pushdown::Transition.create( transition_type, transition_name )
152
+ end
153
+ end
154
+
109
155
  end # class Pushdown::State
110
156
 
@@ -5,26 +5,8 @@ require 'loggability'
5
5
  require 'pluggability'
6
6
 
7
7
  require 'pushdown' unless defined?( Pushdown )
8
+ require 'pushdown/state'
8
9
 
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
10
 
29
11
  # A transition in a Pushdown automaton
30
12
  class Pushdown::Transition
@@ -35,7 +17,7 @@ class Pushdown::Transition
35
17
  log_to :pushdown
36
18
 
37
19
  # Pluggability API -- concrete types live in lib/pushdown/transition/
38
- plugin_prefixes 'pushdown/transition/'
20
+ plugin_prefixes 'pushdown/transition'
39
21
  plugin_exclusions 'spec/**/*'
40
22
 
41
23
 
@@ -53,7 +35,7 @@ class Pushdown::Transition
53
35
  end
54
36
 
55
37
 
56
- ### Create a new Transition with the given +name+. If +data+ is specified, it will be passed
38
+ ### Create a new Transition with the given +name+. If +data+ is specified, it will be passed
57
39
  ### through the transition callbacks on State (State#on_start, State#on_stop, State#on_pause,
58
40
  ### State#on_resume) as it is applied.
59
41
  def initialize( name, data=nil )
@@ -75,11 +57,19 @@ class Pushdown::Transition
75
57
  attr_accessor :data
76
58
 
77
59
 
78
- ### Return a state +stack+ after the transition has been applied.
60
+ ### Return a state +stack+ after the transition has been applied.
79
61
  def apply( stack )
80
62
  raise NotImplementedError, "%p doesn't implement required method #%p" %
81
63
  [ self.class, __method__ ]
82
64
  end
83
65
 
66
+
67
+ ### Return the transition's type as a lowercase Symbol, such as that specified
68
+ ### in transition declarations.
69
+ def type_name
70
+ class_name = self.class.name or return :anonymous
71
+ return class_name.sub( /.*::/, '' ).downcase.to_sym
72
+ end
73
+
84
74
  end # class Pushdown::Transition
85
75
 
data/lib/pushdown.rb CHANGED
@@ -15,17 +15,18 @@ module Pushdown
15
15
  extend Loggability
16
16
 
17
17
  # Package version
18
- VERSION = '0.1.0'
18
+ VERSION = '0.2.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
- autoload :Transition, 'pushdown/transition'
25
- autoload :State, 'pushdown/state'
26
- autoload :Automaton, 'pushdown/automaton'
27
-
28
24
  end # module Pushdown
29
25
 
26
+ require 'pushdown/transition'
27
+ require 'pushdown/state'
28
+ require 'pushdown/automaton'
29
+
30
30
  Pushdown::Transition.plugin_exclusions( '**/spec/pushdown/transition/**' )
31
31
  Pushdown::Transition.load_all
32
+
@@ -1,4 +1,5 @@
1
- #!/usr/bin/env rspec -cfd
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
2
3
 
3
4
  require_relative '../spec_helper'
4
5
 
@@ -0,0 +1,478 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../spec_helper'
5
+
6
+ require 'pushdown/spec_helpers'
7
+
8
+
9
+ RSpec.describe( Pushdown::SpecHelpers ) do
10
+
11
+ include Pushdown::SpecHelpers
12
+
13
+ #
14
+ # Expectation-failure Matchers (stolen from rspec-expectations)
15
+ # See the README for licensing information.
16
+ #
17
+
18
+ def fail
19
+ raise_error( RSpec::Expectations::ExpectationNotMetError )
20
+ end
21
+
22
+ def fail_with( message )
23
+ raise_error( RSpec::Expectations::ExpectationNotMetError, message )
24
+ end
25
+
26
+ def fail_matching( message )
27
+ if String === message
28
+ regexp = /#{Regexp.escape(message)}/
29
+ else
30
+ regexp = message
31
+ end
32
+ raise_error( RSpec::Expectations::ExpectationNotMetError, regexp )
33
+ end
34
+
35
+ def fail_including( *messages )
36
+ raise_error do |err|
37
+ expect( err ).to be_a( RSpec::Expectations::ExpectationNotMetError )
38
+ expect( err.message ).to include( *messages )
39
+ end
40
+ end
41
+
42
+ def dedent( string )
43
+ return string.gsub( /^\t+/, '' ).chomp
44
+ end
45
+
46
+
47
+ let( :state_class ) do
48
+ subclass = Class.new( Pushdown::State )
49
+ end
50
+
51
+ let( :seeking_state_class ) do
52
+ subclass = Class.new( Pushdown::State )
53
+ subclass.singleton_class.attr_accessor( :name )
54
+ subclass.name = 'Acme::State::Seeking'
55
+ return subclass
56
+ end
57
+
58
+
59
+
60
+ describe "transition matcher" do
61
+
62
+ it "passes if a Pushdown::Transition is returned" do
63
+ state_class.attr_accessor( :seeking_state_class )
64
+ state_class.define_method( :update ) do |*|
65
+ return Pushdown::Transition.create( :push, :change, self.seeking_state_class )
66
+ end
67
+
68
+ state = state_class.new
69
+ state.seeking_state_class = seeking_state_class # inject the "seeking" state class
70
+
71
+ expect {
72
+ expect( state ).to transition
73
+ }.to_not raise_error
74
+ end
75
+
76
+
77
+ it "passes if a Symbol that maps to a declared transition is returned" do
78
+ state_class.transition_push( :change, :other )
79
+ state_class.define_method( :update ) do |*|
80
+ return :change
81
+ end
82
+
83
+ state = state_class.new
84
+
85
+ expect {
86
+ expect( state ).to transition
87
+ }.to_not raise_error
88
+ end
89
+
90
+
91
+ it "fails if a Symbol that does not map to a declared transition is returned" do
92
+ state_class.transition_push( :change, :other )
93
+ state_class.define_method( :update ) do |*|
94
+ return :something_else
95
+ end
96
+
97
+ state = state_class.new
98
+
99
+ expect {
100
+ expect( state ).to transition
101
+ }.to fail_matching( /unmapped symbol/i )
102
+ end
103
+
104
+
105
+ it "fails if something other than a Transition or Symbol is returned" do
106
+ state_class.transition_push( :change, :other )
107
+
108
+ state = state_class.new
109
+
110
+ expect {
111
+ expect( state ).to transition
112
+ }.to fail_matching( /expected to transition.*it did not/i )
113
+ end
114
+
115
+ end
116
+
117
+
118
+ describe "transition.via mutator" do
119
+
120
+ it "passes if the state returns a Symbol that maps to the specified kind of transition" do
121
+ state_class.transition_push( :seek, :seeking )
122
+ state_class.define_method( :update ) do |*|
123
+ return :seek
124
+ end
125
+
126
+ state = state_class.new
127
+
128
+ expect {
129
+ expect( state ).to transition.via( :push )
130
+ }.to_not raise_error
131
+ end
132
+
133
+
134
+ it "passes if the state returns a Pushdown::Transition of the correct type" do
135
+ state_class.attr_accessor( :seeking_state_class )
136
+ state_class.define_method( :update ) do |*|
137
+ return Pushdown::Transition.create( :push, :seek, self.seeking_state_class )
138
+ end
139
+
140
+ state = state_class.new
141
+ state.seeking_state_class = seeking_state_class # inject the "seeking" state class
142
+
143
+ expect {
144
+ expect( state ).to transition.via( :push )
145
+ }.to_not raise_error
146
+ end
147
+
148
+
149
+ it "fails with a detailed failure message if the state doesn't transition" do
150
+ state_class.transition_push( :seek, :seeking )
151
+
152
+ state = state_class.new
153
+
154
+ expect {
155
+ expect( state ).to transition.via( :push )
156
+ }.to fail_matching( /expected to transition via push.*returned: nil/i )
157
+ end
158
+
159
+
160
+ it "fails if the state returns a different kind of Pushdown::Transition" do
161
+ state_class.define_method( :update ) do |*|
162
+ return Pushdown::Transition.create( :pop, :restart )
163
+ end
164
+
165
+ state = state_class.new
166
+
167
+ expect {
168
+ expect( state ).to transition.via( :push )
169
+ }.to fail_matching( /transition via push.*pop/i )
170
+ end
171
+
172
+
173
+ it "fails if the state terutns a Symbol that maps to the wrong kind of transition" do
174
+ state_class.transition_pop( :seek )
175
+ state_class.define_method( :update ) do |*|
176
+ return :seek
177
+ end
178
+
179
+ state = state_class.new
180
+
181
+ expect {
182
+ expect( state ).to transition.via( :push )
183
+ }.to fail_matching( /transition via push.*it returned a pop/i )
184
+ end
185
+
186
+ end
187
+
188
+
189
+ describe "transition.to mutator" do
190
+
191
+ it "passes if the state returns a Symbol that maps to a transition to the specified state" do
192
+ state_class.transition_push( :seek, :seeking )
193
+ state_class.define_method( :update ) do |*|
194
+ return :seek
195
+ end
196
+
197
+ state = state_class.new
198
+
199
+ expect {
200
+ expect( state ).to transition.to( :seeking )
201
+ }.to_not raise_error
202
+ end
203
+
204
+
205
+ it "passes if the state returns a Pushdown::Transition with the correct target state" do
206
+ state_class.attr_accessor( :seeking_state_class )
207
+ state_class.define_method( :update ) do |*|
208
+ return Pushdown::Transition.create( :push, :seek, self.seeking_state_class )
209
+ end
210
+
211
+ state = state_class.new
212
+ state.seeking_state_class = seeking_state_class # inject the "seeking" state class
213
+
214
+ expect {
215
+ expect( state ).to transition.to( :seeking )
216
+ }.to_not raise_error
217
+ end
218
+
219
+
220
+ it "fails if the state returns a Symbol that maps to a transition to a different state" do
221
+ state_class.transition_push( :seek, :seeking )
222
+ state_class.define_method( :update ) do |*|
223
+ return :seek
224
+ end
225
+
226
+ state = state_class.new
227
+
228
+ expect {
229
+ expect( state ).to transition.to( :broadcasting )
230
+ }.to fail_matching( /broadcasting.*seeking/i )
231
+ end
232
+
233
+
234
+ it "fails with a detailed failure message if the state doesn't transition" do
235
+ state_class.transition_push( :seek, :seeking )
236
+
237
+ state = state_class.new
238
+
239
+ expect {
240
+ expect( state ).to transition.to( :seeking )
241
+ }.to fail_matching( /to seeking.*returned: nil/i )
242
+ end
243
+
244
+
245
+ it "fails if a Pushdown::Transition with a different target state is returned" do
246
+ state_class.attr_accessor( :seeking_state_class )
247
+ state_class.define_method( :update ) do |*|
248
+ return Pushdown::Transition.create( :push, :seek, self.seeking_state_class )
249
+ end
250
+
251
+ state = state_class.new
252
+ state.seeking_state_class = seeking_state_class # inject the "seeking" state class
253
+
254
+ expect {
255
+ expect( state ).to transition.to( :other )
256
+ }.to fail_matching( /other.*seeking/i )
257
+ end
258
+
259
+ end
260
+
261
+
262
+ describe "composed matcher" do
263
+
264
+ it "passes if both .to and .via are specified and match" do
265
+ state_class.transition_push( :seek, :seeking )
266
+ state_class.define_method( :update ) do |*|
267
+ return :seek
268
+ end
269
+
270
+ state = state_class.new
271
+
272
+ expect {
273
+ expect( state ).to transition.via( :push ).to( :seeking )
274
+ }.to_not raise_error
275
+ end
276
+
277
+
278
+ it "fails if both .to and .via are specified and .to doesn't match" do
279
+ state_class.transition_push( :seek, :broadcasting )
280
+ state_class.define_method( :update ) do |*|
281
+ return :seek
282
+ end
283
+
284
+ state = state_class.new
285
+
286
+ expect {
287
+ expect( state ).to transition.via( :push ).to( :seeking )
288
+ }.to fail_matching( /seeking.*broadcasting/i )
289
+ end
290
+
291
+
292
+ it "fails if both .to and .via are specified and .via doesn't match" do
293
+ state_class.transition_push( :seek, :broadcasting )
294
+ state_class.define_method( :update ) do |*|
295
+ return :seek
296
+ end
297
+
298
+ state = state_class.new
299
+
300
+ expect {
301
+ expect( state ).to transition.via( :switch ).to( :broadcasting )
302
+ }.to fail_matching( /switch.*push/i )
303
+ end
304
+
305
+
306
+ it "fails if both .to and .via are specified and neither match" do
307
+ state_class.transition_push( :seek, :broadcasting )
308
+ state_class.define_method( :update ) do |*|
309
+ return :seek
310
+ end
311
+
312
+ state = state_class.new
313
+
314
+ expect {
315
+ expect( state ).to transition.via( :switch ).to( :seeking )
316
+ }.to fail_matching( /switch.*seeking.*push.*broadcasting/i )
317
+ end
318
+
319
+ end
320
+
321
+
322
+ describe "operation mutators" do
323
+
324
+ it "allows the #update operation to be explicitly specified" do
325
+ state_class.transition_push( :change, :other )
326
+ state_class.define_method( :update ) do |*|
327
+ return :change
328
+ end
329
+ state_class.define_method( :on_event ) do |*|
330
+ return nil
331
+ end
332
+
333
+ state = state_class.new
334
+
335
+ expect {
336
+ expect( state ).to transition.on_update
337
+ }.to_not raise_error
338
+ end
339
+
340
+
341
+ it "allows the #on_event operation to be explicitly specified" do
342
+ state_class.transition_push( :change, :other )
343
+ state_class.define_method( :on_event ) do |*|
344
+ return :change
345
+ end
346
+
347
+ state = state_class.new
348
+
349
+ expect {
350
+ expect( state ).to transition.on_an_event( :foo )
351
+ }.to_not raise_error
352
+ end
353
+
354
+
355
+ it "adds the callback to the description if on_update is specified" do
356
+ state_class.transition_push( :change, :other )
357
+
358
+ state = state_class.new
359
+
360
+ expect {
361
+ expect( state ).to transition.on_update
362
+ }.to fail_matching( /transition when #update is called/i )
363
+ end
364
+
365
+
366
+ it "adds the callback to the description if on_an_event is specified" do
367
+ state_class.transition_push( :change, :other )
368
+
369
+ state = state_class.new
370
+
371
+ expect {
372
+ expect( state ).to transition.on_an_event( :foo )
373
+ }.to fail_matching( /transition when #on_event is called with :foo/i )
374
+ end
375
+
376
+ end
377
+
378
+ describe "negated matcher" do
379
+
380
+ it "succeeds if a Symbol that does not map to a declared transition is returned" do
381
+ state_class.transition_push( :change, :other )
382
+ state_class.define_method( :update ) do |*|
383
+ return :something_else
384
+ end
385
+
386
+ state = state_class.new
387
+
388
+ expect {
389
+ expect( state ).not_to transition
390
+ }.to_not raise_error
391
+ end
392
+
393
+
394
+ it "succeeds if something other than a Transition or Symbol is returned" do
395
+ state_class.transition_push( :change, :other )
396
+
397
+ state = state_class.new
398
+
399
+ expect {
400
+ expect( state ).not_to transition
401
+ }.to_not raise_error
402
+ end
403
+
404
+
405
+ it "succeeds if a specific transition is given, but a different one is returned" do
406
+ state_class.transition_push( :change, :other )
407
+ state_class.define_method( :update ) do |*|
408
+ return :change
409
+ end
410
+
411
+ state = state_class.new
412
+
413
+ expect {
414
+ expect( state ).not_to transition.via( :switch )
415
+ }.to_not raise_error
416
+ end
417
+
418
+
419
+ it "succeeds if a target state is given, but a different one is returned" do
420
+ state_class.transition_push( :change, :broadcasting )
421
+ state_class.define_method( :update ) do |*|
422
+ return :change
423
+ end
424
+
425
+ state = state_class.new
426
+
427
+ expect {
428
+ expect( state ).not_to transition.via( :push ).to( :seeking )
429
+ }.to_not raise_error
430
+ end
431
+
432
+
433
+ it "fails if a Pushdown::Transition is returned" do
434
+ state_class.attr_accessor( :seeking_state_class )
435
+ state_class.define_method( :update ) do |*|
436
+ return Pushdown::Transition.create( :push, :change, self.seeking_state_class )
437
+ end
438
+
439
+ state = state_class.new
440
+ state.seeking_state_class = seeking_state_class # inject the "seeking" state class
441
+
442
+ expect {
443
+ expect( state ).not_to transition
444
+ }.to fail_matching( /not to transition, but it did/i )
445
+ end
446
+
447
+
448
+ it "fails if a Symbol that maps to a declared transition is returned" do
449
+ state_class.transition_push( :change, :other )
450
+ state_class.define_method( :update ) do |*|
451
+ return :change
452
+ end
453
+
454
+ state = state_class.new
455
+
456
+ expect {
457
+ expect( state ).not_to transition
458
+ }.to fail_matching( /not to transition, but it did/i )
459
+ end
460
+
461
+
462
+ it "fails if a type and state are specified and they describe the returned transition" do
463
+ state_class.transition_push( :seek, :seeking )
464
+ state_class.define_method( :update ) do |*|
465
+ return :seek
466
+ end
467
+
468
+ state = state_class.new
469
+
470
+ expect {
471
+ expect( state ).not_to transition.via( :push ).to( :seeking )
472
+ }.to fail_matching( /not.*via push to seeking, but it did/i )
473
+ end
474
+
475
+ end
476
+
477
+
478
+ end
@@ -1,4 +1,5 @@
1
- #!/usr/bin/env rspec -cfd
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
2
3
 
3
4
  require_relative '../spec_helper'
4
5
 
@@ -14,7 +15,16 @@ RSpec.describe( Pushdown::State ) do
14
15
  end
15
16
 
16
17
  let( :starting_state_class ) do
17
- Class.new( described_class )
18
+ Class.new( subclass )
19
+ end
20
+
21
+ let( :automaton_class ) do
22
+ extended_class = Class.new
23
+ extended_class.extend( Pushdown::Automaton )
24
+ extended_class.const_set( :Starting, starting_state_class )
25
+ extended_class.pushdown_state( :state, initial_state: :starting )
26
+
27
+ return extended_class
18
28
  end
19
29
 
20
30
 
@@ -23,7 +33,25 @@ RSpec.describe( Pushdown::State ) do
23
33
  end
24
34
 
25
35
 
26
- describe "event handlers" do
36
+ it "knows what the name of its type is" do
37
+ starting_state_class.singleton_class.attr_accessor :name
38
+ starting_state_class.name = 'Acme::State::Starting'
39
+
40
+ state = starting_state_class.new
41
+
42
+ expect( state.type_name ).to eq( :starting )
43
+ end
44
+
45
+
46
+ it "handles anonymous classes for #type_name" do
47
+ transition = starting_state_class.new
48
+
49
+ expect( transition.type_name ).to eq( :anonymous )
50
+ end
51
+
52
+
53
+
54
+ describe "transition callbacks" do
27
55
 
28
56
  it "has a default (no-op) callback for when it is added to the stack" do
29
57
  instance = subclass.new
@@ -51,19 +79,29 @@ RSpec.describe( Pushdown::State ) do
51
79
  end
52
80
 
53
81
 
54
- describe "update handlers" do
82
+ describe "event handlers" do
55
83
 
56
- it "has a default (no-op) interval callback for when it is on the stack" do
84
+ it "has a default (no-op) event callback" do
57
85
  instance = subclass.new
58
- expect( instance.shadow_update(state_data) ).to be_nil
86
+ expect( instance.on_event(:an_event) ).to be_nil
59
87
  end
60
88
 
89
+ end
90
+
91
+
92
+ describe "interval event handlers" do
61
93
 
62
94
  it "has a default (no-op) interval callback for when it is current" do
63
95
  instance = subclass.new
64
96
  expect( instance.update(state_data) ).to be_nil
65
97
  end
66
98
 
99
+
100
+ it "has a default (no-op) interval callback for when it is on the stack" do
101
+ instance = subclass.new
102
+ expect( instance.shadow_update(state_data) ).to be_nil
103
+ end
104
+
67
105
  end
68
106
 
69
107
 
@@ -84,5 +122,21 @@ RSpec.describe( Pushdown::State ) do
84
122
  end
85
123
 
86
124
 
125
+ describe "transition creation" do
126
+
127
+ it "can create a transition it has declared" do
128
+ subclass.transition_push( :start, :starting )
129
+ instance = subclass.new
130
+
131
+ automaton = automaton_class.new
132
+
133
+ result = instance.transition( :start, automaton, :state )
134
+ expect( result ).to be_a( Pushdown::Transition::Push )
135
+ expect( result.name ).to eq( :start )
136
+ expect( result.data ).to be_nil
137
+ end
138
+
139
+ end
140
+
87
141
  end
88
142
 
@@ -1,4 +1,5 @@
1
- #!/usr/bin/env rspec -cfd
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
2
3
 
3
4
  require_relative '../../spec_helper'
4
5
 
@@ -1,4 +1,5 @@
1
- #!/usr/bin/env rspec -cfd
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
2
3
 
3
4
  require_relative '../../spec_helper'
4
5
 
@@ -1,4 +1,5 @@
1
- #!/usr/bin/env rspec -cfd
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
2
3
 
3
4
  require_relative '../../spec_helper'
4
5
 
@@ -1,4 +1,5 @@
1
- #!/usr/bin/env rspec -cfd
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
2
3
 
3
4
  require_relative '../../spec_helper'
4
5
 
@@ -1,4 +1,5 @@
1
- #!/usr/bin/env rspec -cfd
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
2
3
 
3
4
  require_relative '../spec_helper'
4
5
 
@@ -68,6 +69,23 @@ RSpec.describe( Pushdown::Transition ) do
68
69
  expect( result.last ).to be_a( state_class )
69
70
  end
70
71
 
72
+
73
+ it "knows what the name of its type is" do
74
+ subclass.singleton_class.attr_accessor :name
75
+ subclass.name = 'Acme::Transition::Frobnicate'
76
+
77
+ transition = subclass.new( :rejigger, state_class )
78
+
79
+ expect( transition.type_name ).to eq( :frobnicate )
80
+ end
81
+
82
+
83
+ it "handles anonymous classes for #type_name" do
84
+ transition = subclass.new( :rejigger, state_class )
85
+
86
+ expect( transition.type_name ).to eq( :anonymous )
87
+ end
88
+
71
89
  end
72
90
 
73
91
 
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pushdown
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Granger
@@ -33,7 +33,7 @@ cert_chain:
33
33
  MCh97sQ/Z/MOusb5+QddBmB+k8EicXyGNl4b5L4XpL7fIQu+Y96TB3JEJlShxFD9
34
34
  k9FjI4d9EP54gS/4
35
35
  -----END CERTIFICATE-----
36
- date: 2021-08-27 00:00:00.000000000 Z
36
+ date: 2021-08-31 00:00:00.000000000 Z
37
37
  dependencies:
38
38
  - !ruby/object:Gem::Dependency
39
39
  name: loggability
@@ -105,6 +105,7 @@ files:
105
105
  - lib/pushdown.rb
106
106
  - lib/pushdown/automaton.rb
107
107
  - lib/pushdown/exceptions.rb
108
+ - lib/pushdown/spec_helpers.rb
108
109
  - lib/pushdown/state.rb
109
110
  - lib/pushdown/transition.rb
110
111
  - lib/pushdown/transition/pop.rb
@@ -112,6 +113,7 @@ files:
112
113
  - lib/pushdown/transition/replace.rb
113
114
  - lib/pushdown/transition/switch.rb
114
115
  - spec/pushdown/automaton_spec.rb
116
+ - spec/pushdown/spec_helpers_spec.rb
115
117
  - spec/pushdown/state_spec.rb
116
118
  - spec/pushdown/transition/pop_spec.rb
117
119
  - spec/pushdown/transition/push_spec.rb
metadata.gz.sig CHANGED
Binary file