state-fu 0.11.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|