pushdown 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|