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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/History.md +8 -0
- data/README.md +34 -1
- data/lib/pushdown/automaton.rb +51 -59
- data/lib/pushdown/spec_helpers.rb +263 -0
- data/lib/pushdown/state.rb +49 -3
- data/lib/pushdown/transition.rb +12 -22
- data/lib/pushdown.rb +6 -5
- data/spec/pushdown/automaton_spec.rb +2 -1
- data/spec/pushdown/spec_helpers_spec.rb +478 -0
- data/spec/pushdown/state_spec.rb +60 -6
- data/spec/pushdown/transition/pop_spec.rb +2 -1
- data/spec/pushdown/transition/push_spec.rb +2 -1
- data/spec/pushdown/transition/replace_spec.rb +2 -1
- data/spec/pushdown/transition/switch_spec.rb +2 -1
- data/spec/pushdown/transition_spec.rb +19 -1
- data.tar.gz.sig +0 -0
- metadata +4 -2
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: de5a87ff7d9744a6c71e977a4d5e2fbe03be08efc393d5fc8c84b7a9f4f720c2
|
4
|
+
data.tar.gz: 6a94eca59001d1640a5676e9a135b56690c6e20f2ec75e49740b68b036005870
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
|
data/lib/pushdown/automaton.rb
CHANGED
@@ -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
|
+
|
data/lib/pushdown/state.rb
CHANGED
@@ -43,11 +43,20 @@ class Pushdown::State
|
|
43
43
|
self.transitions[ transition_name ] = [ type, *args ]
|
44
44
|
end
|
45
45
|
|
46
|
-
method_name = "transition_
|
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
|
-
#
|
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
|
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
|
|
data/lib/pushdown/transition.rb
CHANGED
@@ -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.
|
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
|
+
|
@@ -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
|
data/spec/pushdown/state_spec.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
|
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(
|
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
|
-
|
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 "
|
82
|
+
describe "event handlers" do
|
55
83
|
|
56
|
-
it "has a default (no-op)
|
84
|
+
it "has a default (no-op) event callback" do
|
57
85
|
instance = subclass.new
|
58
|
-
expect( instance.
|
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
|
-
|
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.
|
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-
|
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
|