state-fu 0.11.1
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.
- data/LICENSE +40 -0
- data/README.textile +293 -0
- data/Rakefile +114 -0
- data/lib/binding.rb +292 -0
- data/lib/event.rb +192 -0
- data/lib/executioner.rb +120 -0
- data/lib/hooks.rb +39 -0
- data/lib/interface.rb +132 -0
- data/lib/lathe.rb +538 -0
- data/lib/machine.rb +184 -0
- data/lib/method_factory.rb +243 -0
- data/lib/persistence.rb +116 -0
- data/lib/persistence/active_record.rb +34 -0
- data/lib/persistence/attribute.rb +47 -0
- data/lib/persistence/base.rb +100 -0
- data/lib/persistence/relaxdb.rb +23 -0
- data/lib/persistence/session.rb +7 -0
- data/lib/sprocket.rb +58 -0
- data/lib/state-fu.rb +56 -0
- data/lib/state.rb +48 -0
- data/lib/support/active_support_lite/array.rb +9 -0
- data/lib/support/active_support_lite/array/access.rb +60 -0
- data/lib/support/active_support_lite/array/conversions.rb +202 -0
- data/lib/support/active_support_lite/array/extract_options.rb +21 -0
- data/lib/support/active_support_lite/array/grouping.rb +109 -0
- data/lib/support/active_support_lite/array/random_access.rb +13 -0
- data/lib/support/active_support_lite/array/wrapper.rb +25 -0
- data/lib/support/active_support_lite/blank.rb +67 -0
- data/lib/support/active_support_lite/cattr_reader.rb +57 -0
- data/lib/support/active_support_lite/keys.rb +57 -0
- data/lib/support/active_support_lite/misc.rb +59 -0
- data/lib/support/active_support_lite/module.rb +1 -0
- data/lib/support/active_support_lite/module/delegation.rb +130 -0
- data/lib/support/active_support_lite/object.rb +9 -0
- data/lib/support/active_support_lite/string.rb +38 -0
- data/lib/support/active_support_lite/symbol.rb +16 -0
- data/lib/support/applicable.rb +41 -0
- data/lib/support/arrays.rb +197 -0
- data/lib/support/core_ext.rb +90 -0
- data/lib/support/exceptions.rb +106 -0
- data/lib/support/has_options.rb +16 -0
- data/lib/support/logger.rb +165 -0
- data/lib/support/methodical.rb +17 -0
- data/lib/support/no_stdout.rb +55 -0
- data/lib/support/plotter.rb +62 -0
- data/lib/support/vizier.rb +300 -0
- data/lib/tasks/spec_last.rake +55 -0
- data/lib/tasks/state_fu.rake +57 -0
- data/lib/transition.rb +338 -0
- data/lib/transition_query.rb +224 -0
- data/spec/custom_formatter.rb +49 -0
- data/spec/features/binding_and_transition_helper_mixin_spec.rb +111 -0
- data/spec/features/method_missing_only_once_spec.rb +28 -0
- data/spec/features/not_requirements_spec.rb +118 -0
- data/spec/features/plotter_spec.rb +97 -0
- data/spec/features/shared_log_spec.rb +7 -0
- data/spec/features/singleton_machine_spec.rb +39 -0
- data/spec/features/state_and_array_options_accessor_spec.rb +47 -0
- data/spec/features/transition_boolean_comparison_spec.rb +101 -0
- data/spec/helper.rb +13 -0
- data/spec/integration/active_record_persistence_spec.rb +202 -0
- data/spec/integration/binding_extension_spec.rb +41 -0
- data/spec/integration/class_accessor_spec.rb +117 -0
- data/spec/integration/event_definition_spec.rb +74 -0
- data/spec/integration/example_01_document_spec.rb +133 -0
- data/spec/integration/example_02_string_spec.rb +88 -0
- data/spec/integration/instance_accessor_spec.rb +97 -0
- data/spec/integration/lathe_extension_spec.rb +67 -0
- data/spec/integration/machine_duplication_spec.rb +101 -0
- data/spec/integration/relaxdb_persistence_spec.rb +97 -0
- data/spec/integration/requirement_reflection_spec.rb +270 -0
- data/spec/integration/state_definition_spec.rb +163 -0
- data/spec/integration/transition_spec.rb +1033 -0
- data/spec/spec.opts +9 -0
- data/spec/spec_helper.rb +132 -0
- data/spec/state_fu_spec.rb +948 -0
- data/spec/units/binding_spec.rb +192 -0
- data/spec/units/event_spec.rb +214 -0
- data/spec/units/exceptions_spec.rb +82 -0
- data/spec/units/lathe_spec.rb +570 -0
- data/spec/units/machine_spec.rb +229 -0
- data/spec/units/method_factory_spec.rb +366 -0
- data/spec/units/sprocket_spec.rb +69 -0
- data/spec/units/state_spec.rb +59 -0
- metadata +171 -0
data/LICENSE
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# State Fu
|
2
|
+
#
|
3
|
+
# The original master repository for State-Fu is at github:
|
4
|
+
# http://github.com/davidlee/state-fu
|
5
|
+
#
|
6
|
+
# Original Author: David Lee (2009)
|
7
|
+
# http://github.com/davidlee
|
8
|
+
#
|
9
|
+
# Thanks to: Ryan Allen - ryan-allen/workflow
|
10
|
+
# John Barnette - jbarnett/stateful
|
11
|
+
# Scott Barron - rubyist/aasm
|
12
|
+
#
|
13
|
+
# and other rubyists too numerous to mention, for the inspiration
|
14
|
+
#
|
15
|
+
# This software is released under this BSD license:
|
16
|
+
#
|
17
|
+
# Copyright (c) 2009, David Lee. All rights reserved.
|
18
|
+
#
|
19
|
+
# Redistribution and use in source and binary forms, with or without
|
20
|
+
# modification, are permitted provided that the following conditions
|
21
|
+
# are met:
|
22
|
+
#
|
23
|
+
# * Redistributions of source code must retain the above copyright
|
24
|
+
# notice, this list of conditions and the following disclaimer.
|
25
|
+
# * Redistributions in binary form must reproduce the above copyright
|
26
|
+
# notice, this list of conditions and the following disclaimer in the
|
27
|
+
# documentation and/or other materials provided with the distribution.
|
28
|
+
#
|
29
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
30
|
+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
31
|
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
32
|
+
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
33
|
+
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
34
|
+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
35
|
+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
36
|
+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
37
|
+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
38
|
+
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
39
|
+
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
40
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
data/README.textile
ADDED
@@ -0,0 +1,293 @@
|
|
1
|
+
h1. StateFu
|
2
|
+
|
3
|
+
h2. What is it?
|
4
|
+
|
5
|
+
StateFu is another Ruby state machine.
|
6
|
+
|
7
|
+
h2. What is a state machine?
|
8
|
+
|
9
|
+
Finite state machines are a model for program behaviour; like
|
10
|
+
object-oriented programming, they provide an abstract way to think
|
11
|
+
about a domain.
|
12
|
+
|
13
|
+
In a finite state machine, there are a number of discrete states. Only
|
14
|
+
one state may be occupied at any given time (hence the "finite").
|
15
|
+
|
16
|
+
States are linked together by events, and there are rules which govern
|
17
|
+
when or how transitions between states can occur. Actions may be fired
|
18
|
+
on entry to or exit from a state, or when a certain transition occurs.
|
19
|
+
|
20
|
+
h2. Why is StateFu different to the other twenty state machines for Ruby?
|
21
|
+
|
22
|
+
State machines are potentially a powerful way to simplify and
|
23
|
+
structure a lot of problems. They can be used to:
|
24
|
+
|
25
|
+
* succinctly define the grammar of a networking protocol or a
|
26
|
+
configuration DSL
|
27
|
+
|
28
|
+
* clearly and compactly describe complex logic, which might otherwise
|
29
|
+
be difficult to understand by playing "follow the rabbit" through
|
30
|
+
methods thick with implementation details
|
31
|
+
|
32
|
+
* serialize and process multiple revisions of changing business rules
|
33
|
+
|
34
|
+
* provide an abstract representation of program or domain behaviour
|
35
|
+
which can be introspected, edited, queried and executed on the fly,
|
36
|
+
in a high-level and human-readable format
|
37
|
+
|
38
|
+
* provide a straightforward and easy way to record and validate
|
39
|
+
"status" information, especially when there are rules governing
|
40
|
+
when and how it can be updated
|
41
|
+
|
42
|
+
* reduce proliferation of classes and modules, or easily define and
|
43
|
+
control functionally related groups of objects, by defining
|
44
|
+
behaviours on interacting components of a state machine
|
45
|
+
|
46
|
+
* elegantly implement simple building blocks like stacks / queues,
|
47
|
+
parsers, schedulers, automata, etc
|
48
|
+
|
49
|
+
StateFu was written from the ground up with one goal in mind: to be
|
50
|
+
over-engineered. It is designed to make truly ambitious use of state
|
51
|
+
machines not only viable, but strongly advantageous in many situations.
|
52
|
+
|
53
|
+
It is designed in the very opposite vein to the intentional minimalism
|
54
|
+
of most ruby state machine projects; it is tasked with taking on a
|
55
|
+
great deal of complexity and functionality, and abstracting it behind
|
56
|
+
a nice DSL, so that the code which *you* have to maintain is shorter and
|
57
|
+
clearer.
|
58
|
+
|
59
|
+
StateFu allows you to:
|
60
|
+
|
61
|
+
* give a class any number of machines
|
62
|
+
|
63
|
+
* define behaviours on state entry / exit; before, after or during
|
64
|
+
execution of a particular event; or before / after every transition
|
65
|
+
in a given machine
|
66
|
+
|
67
|
+
* give any object own its own private, "singleton" machines,
|
68
|
+
which are unique to that object and modifiable at runtime.
|
69
|
+
|
70
|
+
* create events with any number of origin or target states
|
71
|
+
|
72
|
+
* define and query guard conditions / transition requirements,
|
73
|
+
to establish rules about when a transition is valid
|
74
|
+
|
75
|
+
* use powerful reflection and logging capabilities to easily expose
|
76
|
+
and debug the operation of your machines
|
77
|
+
|
78
|
+
* automatically and unobtrusively define methods for querying each
|
79
|
+
state and event, and for firing transitions
|
80
|
+
|
81
|
+
* easily find out which transitions are valid at any given time
|
82
|
+
|
83
|
+
* generate descriptive, contextual messages when a transition is
|
84
|
+
invalid
|
85
|
+
|
86
|
+
* halt a transition during execution
|
87
|
+
|
88
|
+
* easily extend StateFu's DSL to match the problem domain
|
89
|
+
|
90
|
+
* fire transitions with a payload of arguments and program context,
|
91
|
+
which is available to guard conditions, event hooks, and
|
92
|
+
requirement messages when they are evaluated
|
93
|
+
|
94
|
+
* use a lovely, simple and flexible API which gives you plenty of
|
95
|
+
choices about how to describe your problem domain; choose (or
|
96
|
+
build) a programming style which suits the task at hand from an
|
97
|
+
expressive range of options
|
98
|
+
|
99
|
+
* store arbitrary meta-data on any component of StateFu - a simple
|
100
|
+
but extremely powerful tool for integration with almost anything.
|
101
|
+
|
102
|
+
* flexible and helpful logging out of the box - will use the Rails
|
103
|
+
logger if you're in a Rails project, or standalone logging to
|
104
|
+
STDOUT or a file. Configurable loglevel and message prefixes help
|
105
|
+
StateFu be a good citizen in a shared application log.
|
106
|
+
|
107
|
+
* automatically generate diagrams of state machines / workflows with graphviz
|
108
|
+
|
109
|
+
* use an ActiveRecord field for state persistence, or a regular
|
110
|
+
attribute - or use both, on the same class, for different
|
111
|
+
machines. If an appropriate ActiveRecord field exists for a
|
112
|
+
machine, it will be used. Otherwise, an attr_accessor will be used
|
113
|
+
(and created, if necessary).
|
114
|
+
|
115
|
+
* customising the persistence mechanism (eg to use a Rails session,
|
116
|
+
or a text file, or your choice of ORM) is usually as easy as
|
117
|
+
defining a getter and setter method for the persistence field, and
|
118
|
+
a rule about when to use it. If you want to use StateFu with a
|
119
|
+
persistence mechanism which is not yet supported, send me a message.
|
120
|
+
|
121
|
+
* StateFu is fast, lightweight and useful enough to use in any ruby
|
122
|
+
project - works with Rails but does not require it.
|
123
|
+
|
124
|
+
h2. Still not sold?
|
125
|
+
|
126
|
+
StateFu is forged from a reassuringly dense but unidentifiable metal
|
127
|
+
which comes only from the rarest of meteorites, and it ticks when you
|
128
|
+
hold it up to your ear.[1]
|
129
|
+
|
130
|
+
It is elegant, powerful and transparent enough that you can use
|
131
|
+
it to drive substantial parts of your application, and actually want
|
132
|
+
to do so.
|
133
|
+
|
134
|
+
It is designed as a library for authors, as well as users, of
|
135
|
+
libraries: StateFu goes to great lengths to impose very few limits on
|
136
|
+
your ability to introspect, manipulate and extend the core features.
|
137
|
+
|
138
|
+
It is also delightfully elegant and easy to use for simple things:
|
139
|
+
|
140
|
+
<pre><code>
|
141
|
+
|
142
|
+
class Document < ActiveRecord::Base
|
143
|
+
include StateFu
|
144
|
+
|
145
|
+
def update_rss
|
146
|
+
puts "new feed!"
|
147
|
+
# ... do something here
|
148
|
+
end
|
149
|
+
|
150
|
+
machine( :status ) do
|
151
|
+
state :draft do
|
152
|
+
event :publish, :to => :published
|
153
|
+
end
|
154
|
+
|
155
|
+
state :published do
|
156
|
+
on_entry :update_rss
|
157
|
+
requires :author # a database column
|
158
|
+
end
|
159
|
+
|
160
|
+
event :delete, :from => :ALL, :to => :deleted do
|
161
|
+
execute :destroy
|
162
|
+
end
|
163
|
+
|
164
|
+
# save all states once transition is complete.
|
165
|
+
# this wants to be last, as it iterates over each state which is
|
166
|
+
# already defined.
|
167
|
+
states do
|
168
|
+
accepted { object.save! }
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
my_doc = Document.new
|
174
|
+
|
175
|
+
my_doc.status # returns a StateFu::Binding, which lets us access the 'Fu
|
176
|
+
my_doc.status.state => 'draft' # if this wasn't already a database column or attribute, an
|
177
|
+
# attribute has been created to keep track of the state
|
178
|
+
my_doc.status.name => :draft # the name of the current_state (defaults to the first defined)
|
179
|
+
my_doc.status.publish! # raised => StateFu::RequirementError: [:author]
|
180
|
+
# the author requirement prevented the transition
|
181
|
+
my_doc.status.name => :draft # see? still a draft.
|
182
|
+
my_doc.author = "Susan" # so let's satisfy it ...
|
183
|
+
my_doc.publish! # and try again.
|
184
|
+
"new feed!" # aha - our event hook fires!
|
185
|
+
my_doc.status.name => :published # and the state has been updated.
|
186
|
+
|
187
|
+
</code></pre>
|
188
|
+
|
189
|
+
StateFu works with any modern Ruby ( 1.8.6, 1.8.7, and 1.9.1)
|
190
|
+
|
191
|
+
h2. Getting started
|
192
|
+
|
193
|
+
You can either clone the repository in the usual fashion (eg to
|
194
|
+
yourapp/vendor/plugins/state-fu), or use StateFu as a gem.
|
195
|
+
|
196
|
+
To install as a gem:
|
197
|
+
|
198
|
+
<pre>
|
199
|
+
<code>
|
200
|
+
gem install davidlee-state-fu -s http://gems.github.com
|
201
|
+
</code>
|
202
|
+
</pre>
|
203
|
+
|
204
|
+
To require it in your ruby project:
|
205
|
+
|
206
|
+
<pre>
|
207
|
+
<code>
|
208
|
+
require 'rubygems'
|
209
|
+
require 'state-fu'
|
210
|
+
</code>
|
211
|
+
</pre>
|
212
|
+
|
213
|
+
To install the dependencies for running specs:
|
214
|
+
|
215
|
+
<pre>
|
216
|
+
<code>
|
217
|
+
sudo gem install rspec rr
|
218
|
+
rake # run the specs
|
219
|
+
rake spec:doc # generate specdocs
|
220
|
+
rake doc # generate rdocs
|
221
|
+
rake build # build the gem locally
|
222
|
+
rake install # install it
|
223
|
+
</code>
|
224
|
+
</pre>
|
225
|
+
|
226
|
+
Now you can simply <code>include StateFu</code> in any class you wish to make stateful.
|
227
|
+
|
228
|
+
The spec/ and features/ folders are currently one of the best source
|
229
|
+
of documentation. The documentation is gradually evolving to catch up
|
230
|
+
with the features, but if you have any questions I'm happy to help you
|
231
|
+
get started.
|
232
|
+
|
233
|
+
If you have questions, feature request or ideas, please join the
|
234
|
+
"google group":http://groups.google.com/group/state-fu or send me a
|
235
|
+
message on GitHub.
|
236
|
+
|
237
|
+
h3. A note about ActiveSupport
|
238
|
+
|
239
|
+
StateFu will use ActiveSupport if it is already loaded. If not, it
|
240
|
+
will load its own (heavily trimmed) 'lite' version.
|
241
|
+
|
242
|
+
In most projects this will behave transparently, but it does mean that
|
243
|
+
if you require StateFu *before* other libraries which
|
244
|
+
require ActiveSupport (e.g. ActiveRecord), you may have to
|
245
|
+
explicitly <code>require 'activesupport'</code> before loading the
|
246
|
+
dependent libraries.
|
247
|
+
|
248
|
+
So if you plan to use ActiveSupport in a stand-alone project with
|
249
|
+
StateFu, you should require it before StateFu.
|
250
|
+
|
251
|
+
|
252
|
+
h3. Addditional Resources
|
253
|
+
|
254
|
+
Also see the "issue tracker":http://github.com/davidlee/state-fu/issues
|
255
|
+
|
256
|
+
And the "build monitor":http://runcoderun.com/davidlee/state-fu/
|
257
|
+
|
258
|
+
And the "RDoc":http://rdoc.info/projects/davidlee/state-fu
|
259
|
+
|
260
|
+
|
261
|
+
h3. StateFu is not a complete BPM (Business Process Management) platform
|
262
|
+
|
263
|
+
It's worth noting that StateFu is at it's core a state machine, which
|
264
|
+
strives to be powerful enough to be able to drive many kinds of
|
265
|
+
application behaviour.
|
266
|
+
|
267
|
+
It is not, however, a classical workflow engine on par with Ruote. In
|
268
|
+
StateFu the basic units with which "workflows" are built are states
|
269
|
+
and events; Ruote takes a higher level view, dealing with processes
|
270
|
+
and participants. As a result, it's capable of directly implementing
|
271
|
+
these design patterns:
|
272
|
+
|
273
|
+
http://openwferu.rubyforge.org/patterns.html
|
274
|
+
|
275
|
+
Whereas StateFu cannot, for example, readily model forking / merging
|
276
|
+
of processes (nor does it handles scheduling, process management, etc.
|
277
|
+
|
278
|
+
The author of Ruote, the Ruby Workflow Engine, outlines the difference
|
279
|
+
pretty clearly here:
|
280
|
+
|
281
|
+
http://jmettraux.wordpress.com/2009/07/03/state-machine-workflow-engine/
|
282
|
+
|
283
|
+
If your application can be described with StateFu, you'll likely find
|
284
|
+
it simpler to get running and work with; if not, you may find Ruote,
|
285
|
+
or a combination of the two, suits your needs perfectly.
|
286
|
+
|
287
|
+
h3. Thanks
|
288
|
+
|
289
|
+
* dsturnbull, for patches
|
290
|
+
|
291
|
+
* lachie, benkimball for pointing out README bugs / typos
|
292
|
+
|
293
|
+
* Ryan Allen for his original Workflow library
|
data/Rakefile
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require "spec/rake/spectask"
|
3
|
+
#require 'cucumber/rake/task'
|
4
|
+
require "date"
|
5
|
+
require "fileutils"
|
6
|
+
require "rubygems"
|
7
|
+
|
8
|
+
load File.join( File.dirname(__FILE__),"/lib/tasks/state_fu.rake" )
|
9
|
+
|
10
|
+
module Rakefile
|
11
|
+
def self.windows?
|
12
|
+
/djgpp|(cyg|ms|bcc)win|mingw/ =~ RUBY_PLATFORM
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
load 'lib/tasks/spec_last.rake'
|
17
|
+
load 'lib/tasks/state_fu.rake'
|
18
|
+
|
19
|
+
# to build the gem:
|
20
|
+
#
|
21
|
+
# gem install jeweller
|
22
|
+
# rake build
|
23
|
+
# rake install
|
24
|
+
begin
|
25
|
+
require 'jeweler'
|
26
|
+
Jeweler::Tasks.new do |s| # gemspec (Gem::Specification)
|
27
|
+
s.name = "state-fu"
|
28
|
+
s.rubyforge_project = "state-fu"
|
29
|
+
s.platform = Gem::Platform::RUBY
|
30
|
+
s.has_rdoc = true
|
31
|
+
# s.extra_rdoc_files = ["README.rdoc"]
|
32
|
+
s.summary = "A rich library for state-oriented programming with state machines / workflows"
|
33
|
+
s.description = s.summary
|
34
|
+
s.author = "David Lee"
|
35
|
+
s.email = "david@rubyist.net.au"
|
36
|
+
s.homepage = "http://github.com/davidlee/state-fu"
|
37
|
+
s.require_path = "lib"
|
38
|
+
# s.files = %w(README.rdoc Rakefile) + Dir.glob("{lib,spec}/**/*")
|
39
|
+
s.files = %w(Rakefile) + Dir.glob("{lib,spec}/**/*")
|
40
|
+
end
|
41
|
+
rescue LoadError
|
42
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
43
|
+
end
|
44
|
+
|
45
|
+
namespace :spec do
|
46
|
+
|
47
|
+
desc 'run the nice new specs'
|
48
|
+
Spec::Rake::SpecTask.new(:state_fu) do |t|
|
49
|
+
t.spec_files = FileList["spec/state_fu_spec.rb"]
|
50
|
+
t.spec_opts = ["--options", "spec/spec.opts"]
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
desc "Run all (old) specs"
|
55
|
+
Spec::Rake::SpecTask.new(:all) do |t|
|
56
|
+
t.spec_files = FileList["spec/**/*_spec.rb"]
|
57
|
+
t.spec_opts = ["--options", "spec/spec.opts"]
|
58
|
+
end
|
59
|
+
|
60
|
+
task :skip_slow do
|
61
|
+
ENV['SKIP_SLOW_SPECS'] = 'true'
|
62
|
+
end
|
63
|
+
|
64
|
+
desc "Run all specs, except especially slow ones"
|
65
|
+
task :quick => [:skip_slow, :all]
|
66
|
+
|
67
|
+
desc "Run all specs with profiling & backtrace"
|
68
|
+
Spec::Rake::SpecTask.new(:prof) do |t|
|
69
|
+
t.spec_files = FileList["spec/**/*_spec.rb"]
|
70
|
+
t.spec_opts = ['-c','-b','-u','-f','profile','-R','-L','mtime']
|
71
|
+
end
|
72
|
+
|
73
|
+
desc "Print Specdoc for all specs (eaxcluding plugin specs)"
|
74
|
+
Spec::Rake::SpecTask.new(:doc) do |t|
|
75
|
+
t.spec_files = FileList["spec/**/*_spec.rb"]
|
76
|
+
t.spec_opts = ["--format", "nested","--color"]
|
77
|
+
end
|
78
|
+
|
79
|
+
desc "Run autotest"
|
80
|
+
task :auto do |t|
|
81
|
+
exec 'autospec'
|
82
|
+
end
|
83
|
+
|
84
|
+
task :default => :state_fu
|
85
|
+
end
|
86
|
+
|
87
|
+
desc 'Runs irb in this project\'s context'
|
88
|
+
task :irb do |t|
|
89
|
+
exec 'irb -I lib -I spec -r state-fu -r spec_helper'
|
90
|
+
end
|
91
|
+
|
92
|
+
desc 'Runs rdoc on the project lib directory'
|
93
|
+
task :doc do |t|
|
94
|
+
exec 'rdoc lib/'
|
95
|
+
end
|
96
|
+
|
97
|
+
desc 'Delete logfiles'
|
98
|
+
namespace :log do
|
99
|
+
task :clear do |t|
|
100
|
+
Dir['log/*.log'].each { |log| File.rm(log) }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
begin
|
105
|
+
require 'cucumber/rake/task'
|
106
|
+
|
107
|
+
Cucumber::Rake::Task.new(:features) do |t|
|
108
|
+
t.cucumber_opts = "--format pretty"
|
109
|
+
end
|
110
|
+
rescue LoadError => e
|
111
|
+
end
|
112
|
+
|
113
|
+
task :all => 'spec:all'
|
114
|
+
task :default => :all
|
data/lib/binding.rb
ADDED
@@ -0,0 +1,292 @@
|
|
1
|
+
module StateFu
|
2
|
+
class Binding
|
3
|
+
|
4
|
+
attr_reader :object, :machine, :method_name, :field_name, :persister, :transitions, :options, :target
|
5
|
+
|
6
|
+
|
7
|
+
# the constructor should not be called manually; a binding is
|
8
|
+
# returned when an instance of a class with a StateFu::Machine
|
9
|
+
# calls:
|
10
|
+
#
|
11
|
+
# instance.#state_fu (for the default machine which is called :state_fu),
|
12
|
+
# instance.#state_fu( :<machine_name> ) ,or
|
13
|
+
# instance.#<machine_name>
|
14
|
+
#
|
15
|
+
def initialize( machine, object, method_name, options={} )
|
16
|
+
@machine = machine
|
17
|
+
@object = object
|
18
|
+
@method_name = method_name
|
19
|
+
@transitions = []
|
20
|
+
@options = options.symbolize_keys!
|
21
|
+
@target = singleton? ? object : object.class
|
22
|
+
@field_name = options[:field_name] || @target.state_fu_field_names[method_name]
|
23
|
+
@persister = Persistence.for( self )
|
24
|
+
|
25
|
+
# define event methods on this binding and its @object
|
26
|
+
MethodFactory.new( self ).install!
|
27
|
+
@machine.helpers.inject_into( self )
|
28
|
+
end
|
29
|
+
|
30
|
+
alias_method :o, :object
|
31
|
+
alias_method :obj, :object
|
32
|
+
alias_method :model, :object
|
33
|
+
alias_method :instance, :object
|
34
|
+
|
35
|
+
alias_method :workflow, :machine
|
36
|
+
alias_method :state_machine, :machine
|
37
|
+
|
38
|
+
#
|
39
|
+
# current state
|
40
|
+
#
|
41
|
+
|
42
|
+
# the current State
|
43
|
+
def current_state
|
44
|
+
persister.current_state
|
45
|
+
end
|
46
|
+
alias_method :now, :current_state
|
47
|
+
alias_method :state, :current_state
|
48
|
+
|
49
|
+
# the name, as a Symbol, of the binding's current_state
|
50
|
+
def current_state_name
|
51
|
+
begin
|
52
|
+
current_state.name.to_sym
|
53
|
+
rescue NoMethodError
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
alias_method :name, :current_state_name
|
58
|
+
alias_method :state_name, :current_state_name
|
59
|
+
alias_method :to_sym, :current_state_name
|
60
|
+
|
61
|
+
#
|
62
|
+
# These methods are called from methods defined by MethodFactory.
|
63
|
+
#
|
64
|
+
|
65
|
+
# event_name [target], *args
|
66
|
+
#
|
67
|
+
def find_transition(event, target=nil, *args)
|
68
|
+
target ||= args.last[:to].to_sym rescue nil
|
69
|
+
query = transitions.for_event(event).to(target).with(*args)
|
70
|
+
query.find || query.valid.singular || nil
|
71
|
+
end
|
72
|
+
|
73
|
+
# event_name? [target], *args
|
74
|
+
#
|
75
|
+
def can_transition?(event, target=nil, *args)
|
76
|
+
begin
|
77
|
+
if t = find_transition(event, target, *args)
|
78
|
+
t.valid?(*args)
|
79
|
+
end
|
80
|
+
rescue IllegalTransition, UnknownTarget
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# event_name! [target], *args
|
86
|
+
#
|
87
|
+
def fire_transition!(event, target=nil, *args)
|
88
|
+
find_transition(event, target, *args).fire!
|
89
|
+
end
|
90
|
+
|
91
|
+
#
|
92
|
+
# events
|
93
|
+
#
|
94
|
+
|
95
|
+
# returns a list of Events which can fire from the current_state
|
96
|
+
def events
|
97
|
+
machine.events.select do |e|
|
98
|
+
e.can_transition_from? current_state
|
99
|
+
end.extend EventArray
|
100
|
+
end
|
101
|
+
alias_method :events_from_current_state, :events
|
102
|
+
|
103
|
+
# all states which can be reached from the current_state.
|
104
|
+
# Does not check transition requirements, etc.
|
105
|
+
def next_states
|
106
|
+
events.map(&:targets).compact.flatten.uniq.extend StateArray
|
107
|
+
end
|
108
|
+
|
109
|
+
#
|
110
|
+
# transition validation
|
111
|
+
#
|
112
|
+
|
113
|
+
def transitions(opts={}) # .with(*args)
|
114
|
+
TransitionQuery.new(self, opts)
|
115
|
+
end
|
116
|
+
|
117
|
+
def valid_transitions(*args)
|
118
|
+
transitions.valid.with(*args)
|
119
|
+
end
|
120
|
+
|
121
|
+
def valid_next_states(*args)
|
122
|
+
valid_transitions(*args).targets
|
123
|
+
end
|
124
|
+
|
125
|
+
def valid_events(*args)
|
126
|
+
valid_transitions(*args).events
|
127
|
+
end
|
128
|
+
|
129
|
+
def invalid_events(*args)
|
130
|
+
(events - valid_events(*args)).extend StateArray
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
# initializes a new Transition to the given destination, with the
|
135
|
+
# given *args (to be passed to requirements and hooks).
|
136
|
+
#
|
137
|
+
# If a block is given, it yields the Transition or is executed in
|
138
|
+
# its evaluation context, depending on the arity of the block.
|
139
|
+
def transition( event_or_array, *args, &block )
|
140
|
+
return transitions.with(*args, &block).find(event_or_array)
|
141
|
+
end
|
142
|
+
|
143
|
+
#
|
144
|
+
# next_transition and friends: when there's exactly one valid move
|
145
|
+
#
|
146
|
+
|
147
|
+
# if there is exactly one legal & valid transition which can be fired with
|
148
|
+
# the given (optional) arguments, return it.
|
149
|
+
def next_transition( *args, &block )
|
150
|
+
transitions.with(*args, &block).next
|
151
|
+
end
|
152
|
+
|
153
|
+
# as above but ignoring any transitions whose origin and target are the same
|
154
|
+
def next_transition_excluding_cycles( *args, &block )
|
155
|
+
transitions.not_cyclic.with(*args, &block).next
|
156
|
+
end
|
157
|
+
|
158
|
+
# if there is exactly one state reachable via a transition which
|
159
|
+
# is valid with the given optional arguments, return it.
|
160
|
+
def next_state(*args, &block)
|
161
|
+
transitions.with(*args, &block).next_state
|
162
|
+
end
|
163
|
+
|
164
|
+
# if there is exactly one event which is valid with the given
|
165
|
+
# optional arguments, return it
|
166
|
+
def next_event( *args )
|
167
|
+
transitions.with(*args, &block).next_event
|
168
|
+
end
|
169
|
+
|
170
|
+
# if there is a next_transition, create, fire & return it
|
171
|
+
# otherwise raise an IllegalTransition
|
172
|
+
def next!( *args, &block )
|
173
|
+
if t = next_transition( *args, &block )
|
174
|
+
t.fire!
|
175
|
+
else
|
176
|
+
raise TransitionNotFound.new( self, valid_transitions(*args), "Exactly 1 valid transition required.")
|
177
|
+
end
|
178
|
+
end
|
179
|
+
alias_method :next_transition!, :next!
|
180
|
+
alias_method :next_event!, :next!
|
181
|
+
alias_method :next_state!, :next!
|
182
|
+
|
183
|
+
# if there is a next_transition, return true / false depending on
|
184
|
+
# whether its requirements are met
|
185
|
+
# otherwise, nil
|
186
|
+
def next?( *args, &block )
|
187
|
+
if t = next_transition( *args, &block )
|
188
|
+
t.requirements_met?
|
189
|
+
end
|
190
|
+
end
|
191
|
+
# alias_method :next_state?, :next?
|
192
|
+
# alias_method :next_event?, :next?
|
193
|
+
|
194
|
+
# Cyclic transitions (origin == target)
|
195
|
+
|
196
|
+
# if there is one possible cyclical event, return a transition there
|
197
|
+
# otherwise, maybe we got an event name as an argument?
|
198
|
+
def cycle(event_or_array=nil, *args, &block)
|
199
|
+
if event_or_array.nil?
|
200
|
+
transitions.cyclic.with(*args, &block).singular ||
|
201
|
+
transitions.cyclic.with(*args, &block).valid.singular
|
202
|
+
else
|
203
|
+
transitions.cyclic.with(*args, &block).find(event_or_array)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# if there is a single possible cycle() transition, fire and return it
|
208
|
+
# otherwise raise an IllegalTransition
|
209
|
+
def cycle!(event_or_array=nil, *args, &block )
|
210
|
+
returning cycle(event_or_array, *args, &block ) do |t|
|
211
|
+
raise TransitionNotFound.new( self, transitions.cyclic.with(*args,&block), "Cannot cycle! unless there is exactly one cyclic event") \
|
212
|
+
if t.nil?
|
213
|
+
t.fire!
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# if there is one possible cyclical event, evaluate its
|
218
|
+
# requirements (true/false), else nil
|
219
|
+
def cycle?(event_or_array=nil, *args )
|
220
|
+
if t = cycle(event_or_array, *args )
|
221
|
+
t.requirements_met?
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
# next! without the raise if there's no next transition
|
226
|
+
# TODO SPECME
|
227
|
+
def update!( *args, &block )
|
228
|
+
if t = next_transition( *args, &block )
|
229
|
+
t.fire!
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
#
|
234
|
+
# misc
|
235
|
+
#
|
236
|
+
|
237
|
+
# change the current state of the binding without any
|
238
|
+
# requirements or other sanity checks, or any hooks firing.
|
239
|
+
# Useful for test / spec scenarios, and abusing the framework.
|
240
|
+
def teleport!( target )
|
241
|
+
persister.current_state=( machine.states[target] )
|
242
|
+
end
|
243
|
+
|
244
|
+
# display something sensible that doesn't take up the whole screen
|
245
|
+
def inspect
|
246
|
+
'<#' + self.class.to_s + ' ' +
|
247
|
+
attrs = [[:current_state, state_name.inspect],
|
248
|
+
[:object_type , @object.class],
|
249
|
+
[:method_name , method_name.inspect],
|
250
|
+
[:field_name , field_name.inspect],
|
251
|
+
[:machine , machine.to_s]].
|
252
|
+
map {|x| x.join('=') }.join( " " ) + '>'
|
253
|
+
end
|
254
|
+
|
255
|
+
# let's be == (and hence ===) the current_state_name as a symbol.
|
256
|
+
# a nice little convenience.
|
257
|
+
def == other
|
258
|
+
if other.respond_to?( :to_sym ) && current_state
|
259
|
+
current_state_name == other.to_sym || super( other )
|
260
|
+
else
|
261
|
+
super( other )
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# TODO better name
|
266
|
+
# is this a binding unique to a specific instance (not bound to a class)?
|
267
|
+
def singleton?
|
268
|
+
options[:singleton]
|
269
|
+
end
|
270
|
+
|
271
|
+
# SPECME DOCME OR KILLME
|
272
|
+
def reload()
|
273
|
+
if persister.is_a?( Persistence::ActiveRecord )
|
274
|
+
object.reload
|
275
|
+
end
|
276
|
+
persister.reload
|
277
|
+
self
|
278
|
+
end
|
279
|
+
|
280
|
+
def inspect
|
281
|
+
s = self.to_s
|
282
|
+
s = s[0,s.length-1]
|
283
|
+
s << " object=#{object}"
|
284
|
+
s << " current_state=#{current_state.to_sym.inspect rescue nil}"
|
285
|
+
s << " events=#{events.map(&:to_sym).inspect rescue nil}"
|
286
|
+
s << " machine=#{machine.to_s}"
|
287
|
+
s << ">"
|
288
|
+
s
|
289
|
+
end
|
290
|
+
|
291
|
+
end
|
292
|
+
end
|