pushdown 0.1.0.pre.20210714190141 → 0.4.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: b3aa15b901bc12dcc15430512e8ec06fdaa5bbc98afd222a62892fad5b1fa689
4
- data.tar.gz: 5b4d53652f5a1d0df9c759d6b9226d4780c87cb4258d650295748e8ecd64ab01
3
+ metadata.gz: 07f7a5dfc7c5783cd2fe4e7e610467519ef85ed1a7ff857bce91c58805f6f457
4
+ data.tar.gz: 02a41fb1852f955d282834ea9ff397d98bb66681f3a7f38b26e13d00503a89dc
5
5
  SHA512:
6
- metadata.gz: 5f3143596b9615ab3a538a2b5256fc43649b11ca1cf6a90fa33a616cee74c09d6ba39e4832ad417352fd15a1162a2698daca1aedcd069af4cf2d48b551dd1245
7
- data.tar.gz: 7f1fd1112549fd7cf19c2a472678fbd5dc58d6d07178d7ce84d0a5d7b96293a3e272b691499624fd23de961aff1df50b742bdaeab640e4d0e4c065fa6771b5d7
6
+ metadata.gz: 537d7f75b6265b4a08b20b67d9f547a42b27514e18d46a7dc9d3bd0f2066ae8161424bf92f39d2c25ad8dca2762a3731deb368c259f26c65004e26244a388f54
7
+ data.tar.gz: b5398fee7bc5fe436f39c8ad760118fd6178baadf13362e85402b442fd609d10b5a5b6d73c2b04a49700ab30641be4d4d190d156e5622c0de9ffe19d9557b965
checksums.yaml.gz.sig ADDED
@@ -0,0 +1,2 @@
1
+ ���� ��D����u^DA9�k��;�"z�@ �Ÿ?��HD ���2u.�E3/w^�`�
2
+ �R�d�p��WbɢzXC �`���UDJ-g��c˸�*OZ�yDPc�� �c��&���ב�������J8��Z�Un�M�� -�|K��G@B��n��fW��̊�1�B�����
data/History.md CHANGED
@@ -1,4 +1,33 @@
1
- ## v0.0.1 [YYYY-MM-DD] Michael Granger <ged@FaerieMUD.org>
1
+ # Release History for pushdown
2
+
3
+ ---
4
+ ## v0.4.0 [2021-09-21] Michael Granger <ged@faeriemud.org>
5
+
6
+ Improvements:
7
+
8
+ - Report operator arguments in the spec matcher failure output.
9
+ - Allow the State event callback to accept additional arguments.
10
+
11
+
12
+ ## v0.3.0 [2021-09-01] Michael Granger <ged@faeriemud.org>
13
+
14
+ Change:
15
+
16
+ - Rework how state data is passed around. Instead of passing it through the
17
+ transition callbacks, which proved to be a terrible idea, the States
18
+ themselves take an optional state argument to their constructor.
19
+
20
+
21
+ ## v0.2.0 [2021-08-31] Michael Granger <ged@faeriemud.org>
22
+
23
+ Improvements:
24
+
25
+ - Move responsibility for creating transitions into State.
26
+ - Add spec helpers with a transition matcher.
27
+ - Add missing event handler method to State
28
+
29
+
30
+ ## v0.1.0 [2021-08-27] Michael Granger <ged@faeriemud.org>
2
31
 
3
32
  Initial release.
4
33
 
data/LICENSE.txt ADDED
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2019-2021, Michael Granger
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+
10
+ * Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ * Neither the name of the author/s, nor the names of the project's
15
+ contributors may be used to endorse or promote products derived from this
16
+ software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
22
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md CHANGED
@@ -31,27 +31,61 @@ It's still mostly experimental.
31
31
  $ gem install pushdown
32
32
 
33
33
 
34
- ## Contributing
34
+ ## Development
35
35
 
36
- You can check out the current development source with Mercurial via its
37
- [project page](http://bitbucket.org/ged/pushdown). Or if you prefer
38
- Git, via [its Github mirror](https://github.com/ged/pushdown).
36
+ You can check out the current source with Git via Gitlab:
37
+
38
+ $ hg clone http://hg.sr.ht/~ged/Pushdown
39
+ $ cd Pushdown
39
40
 
40
41
  After checking out the source, run:
41
42
 
42
- $ rake newb
43
+ $ gem install -Ng
44
+ $ rake setup
43
45
 
44
- This task will install any missing dependencies, run the tests/specs,
45
- and generate the API documentation.
46
+ This task will install dependencies, and do any other necessary setup for development.
46
47
 
47
48
 
48
49
  ## Author(s)
49
50
 
50
51
  * Michael Granger <ged@faeriemud.org>
51
52
 
53
+ 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.
54
+
55
+ Thanks also to Alyssa Verkade for the initial idea.
56
+
52
57
 
53
58
  ## License
54
59
 
60
+ This gem includes code from the rspec-expectations gem, used under the
61
+ terms of the MIT License:
62
+
63
+ Copyright (c) 2012 David Chelimsky, Myron Marston
64
+ Copyright (c) 2006 David Chelimsky, The RSpec Development Team
65
+ Copyright (c) 2005 Steven Baker
66
+
67
+ Permission is hereby granted, free of charge, to any person obtaining
68
+ a copy of this software and associated documentation files (the
69
+ "Software"), to deal in the Software without restriction, including
70
+ without limitation the rights to use, copy, modify, merge, publish,
71
+ distribute, sublicense, and/or sell copies of the Software, and to
72
+ permit persons to whom the Software is furnished to do so, subject to
73
+ the following conditions:
74
+
75
+ The above copyright notice and this permission notice shall be
76
+ included in all copies or substantial portions of the Software.
77
+
78
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
79
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
80
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
81
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
82
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
83
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
84
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
85
+
86
+
87
+ Pushdown itself is:
88
+
55
89
  Copyright (c) 2019-2021, Michael Granger
56
90
  All rights reserved.
57
91
 
@@ -80,6 +114,6 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
80
114
  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
81
115
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
82
116
 
83
-
117
+ [amethyst]: https://amethyst.rs/
84
118
  [amethyst-state-manager]: https://book.amethyst.rs/stable/concepts/state.html#state-manager
85
119
 
@@ -0,0 +1,228 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'loggability'
5
+
6
+ require 'pushdown' unless defined?( Pushdown )
7
+
8
+
9
+ # A mixin that adds pushdown-automaton functionality to another module/class.
10
+ module Pushdown::Automaton
11
+ extend Loggability
12
+
13
+ # Loggability API -- log to the pushdown logger
14
+ log_to :pushdown
15
+
16
+
17
+ ### Extension callback -- add some stuff to extending objects.
18
+ def self::extended( object )
19
+ super
20
+
21
+ unless object.respond_to?( :log )
22
+ object.extend( Loggability )
23
+ object.log_to( :pushdown )
24
+ end
25
+
26
+ object.instance_variable_set( :@pushdown_states, {} )
27
+ object.singleton_class.attr_reader( :pushdown_states )
28
+ object.include( Pushdown::Automaton::InstanceMethods )
29
+ end
30
+
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
+
83
+ ### Generate the pushdown API methods for the pushdown automaton with the given
84
+ ### +name+ and install them in the extending +object+.
85
+ def self::install_state_methods( name, object )
86
+ self.log.debug "Installing pushdown methods for %p in %p" % [ name, object ]
87
+ object.attr_reader( "#{name}_stack" )
88
+
89
+ # Relies on the above method having already been declared
90
+ state_method = self.generate_state_method( name, object )
91
+ object.define_method( name, &state_method )
92
+
93
+ event_method = self.generate_event_method( name, object )
94
+ object.define_method( "handle_#{name}_event", &event_method )
95
+
96
+ update_method = self.generate_update_method( name, object )
97
+ object.define_method( "update_#{name}", &update_method )
98
+
99
+ update_method = self.generate_shadow_update_method( name, object )
100
+ object.define_method( "shadow_update_#{name}", &update_method )
101
+
102
+ initial_state_method = self.generate_initial_state_method( name )
103
+ object.define_singleton_method( "initial_#{name}", &initial_state_method )
104
+ end
105
+
106
+
107
+ ### Generate the method used to access the current state object.
108
+ def self::generate_state_method( name, object )
109
+ self.log.debug "Generating current state method for %p: %p" % [ object, name ]
110
+ stack_method = object.instance_method( "#{name}_stack" )
111
+ meth = lambda { stack_method.bind(self).call&.last }
112
+
113
+ return meth
114
+ end
115
+
116
+
117
+ ### Generate the external event handler method for the pushdown state named +name+
118
+ ### on the specified +object+.
119
+ def self::generate_event_method( name, object )
120
+ self.log.debug "Generating event method for %p: handle_%s_event" % [ object, name ]
121
+
122
+ stack_method = object.instance_method( "#{name}_stack" )
123
+ meth = lambda do |event, *args|
124
+ stack = stack_method.bind( self ).call
125
+ current_state = stack.last
126
+
127
+ result = current_state.on_event( event, *args )
128
+ return self.handle_pushdown_result( stack, result, name )
129
+ end
130
+ end
131
+
132
+
133
+ ### Generate the timed update method for the active pushdown state named +name+
134
+ ### on the specified +object+.
135
+ def self::generate_update_method( name, object )
136
+ self.log.debug "Generating update method for %p: update_%s" % [ object, name ]
137
+
138
+ stack_method = object.instance_method( "#{name}_stack" )
139
+ meth = lambda do |*args|
140
+ stack = stack_method.bind( self ).call
141
+ current_state = stack.last
142
+
143
+ result = current_state.update( *args )
144
+ return self.handle_pushdown_result( stack, result, name )
145
+ end
146
+ end
147
+
148
+
149
+ ### Generate the timed update method for every pushdown state named +name+
150
+ ### on the specified +object+.
151
+ def self::generate_shadow_update_method( name, object )
152
+ self.log.debug "Generating shadow update method for %p: shadow_update_%s" % [ object, name ]
153
+
154
+ stack_method = object.instance_method( "#{name}_stack" )
155
+ meth = lambda do |*args|
156
+ stack = stack_method.bind( self ).call
157
+ stack.each do |state|
158
+ state.shadow_update( *args )
159
+ end
160
+
161
+ # :TODO: Calling/return convention? Could do something like #flat_map the
162
+ # results? Or map to a hash keyed by state object? Is it useful enough to justify
163
+ # the object churn of a method that might potentionally be in a hot loop?
164
+ return nil
165
+ end
166
+ end
167
+
168
+
169
+ ### Generate the method that returns the initial state class for a pushdown
170
+ ### state named +name+.
171
+ def self::generate_initial_state_method( name )
172
+ self.log.debug "Generating initial state method for %p" % [ name ]
173
+ return lambda do
174
+ config = self.pushdown_states[ name ]
175
+ class_name = config[ :initial_state ]
176
+ return self.pushdown_state_class( name, class_name )
177
+ end
178
+ end
179
+
180
+
181
+ ### Declare a attribute +name+ which is a pushdown state.
182
+ def pushdown_state( name, initial_state:, states: nil )
183
+ @pushdown_states[ name ] = { initial_state: initial_state, states: states }
184
+
185
+ Pushdown::Automaton.install_state_methods( name, self )
186
+ end
187
+
188
+
189
+ ### Return the Class object for the class named +class_name+ of the pushdown state
190
+ ### +state_name+.
191
+ def pushdown_state_class( state_name, class_name )
192
+ config = self.pushdown_states[ state_name ] or
193
+ raise "No pushdown state named %p" % [ state_name ]
194
+ states = config[ :states ]
195
+
196
+ case states
197
+ when NilClass
198
+ return self.pushdown_inferred_state_class( class_name )
199
+ when Class
200
+ return self.pushdown_pluggable_state_class( states, class_name )
201
+ when Hash
202
+ return states[ class_name ]
203
+ else
204
+ raise "don't know how to derive a state class from %p (%p)" % [ states, states.class ]
205
+ end
206
+ end
207
+
208
+
209
+ ### Return the state class with the name inferred from the given +class_name+.
210
+ def pushdown_inferred_state_class( class_name )
211
+ constant_name = class_name.to_s.capitalize.gsub( /_(\p{Alnum})/ ) do |match|
212
+ match[ 1 ].capitalize
213
+ end
214
+ self.log.debug "Inferred state class for %p is: %s" % [ class_name, constant_name ]
215
+
216
+ return self.const_get( constant_name )
217
+ end
218
+
219
+
220
+ ### Derive a state class object named +class_name+ via the (pluggable)
221
+ ### +state_base_class+.
222
+ def pushdown_pluggable_state_class( state_base_class, class_name )
223
+ return state_base_class.get_subclass( class_name )
224
+ end
225
+
226
+ end # module Pushdown::Automaton
227
+
228
+
@@ -0,0 +1,18 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'pushdown' unless defined?( Pushdown )
5
+ require 'e2mmap'
6
+
7
+ module Pushdown
8
+ extend Exception2MessageMapper
9
+
10
+
11
+ def_exception :Error, "pushdown error"
12
+
13
+ def_exception :StateError, "error in state machine", Pushdown::Error
14
+ def_exception :TransitionError, "error while transitioning", Pushdown::Error
15
+
16
+
17
+ end # module Pushdown
18
+
@@ -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, *args )
107
+ raise ScriptError, "can't specify more than one callback" if self.callback
108
+ @callback = [ :on_event, event, *args ]
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, *args = self.callback
186
+ desc << " when #%s is called" % [ methname ]
187
+ desc << " with %s" % [ args.map(&:inspect).join(', ') ] if !args.empty?
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
+