davidlee-state-fu 0.0.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 +174 -0
- data/Rakefile +87 -0
- data/lib/no_stdout.rb +32 -0
- data/lib/state-fu.rb +93 -0
- data/lib/state_fu/binding.rb +262 -0
- data/lib/state_fu/core_ext.rb +23 -0
- data/lib/state_fu/event.rb +98 -0
- data/lib/state_fu/exceptions.rb +42 -0
- data/lib/state_fu/fu_space.rb +50 -0
- data/lib/state_fu/helper.rb +189 -0
- data/lib/state_fu/hooks.rb +28 -0
- data/lib/state_fu/interface.rb +139 -0
- data/lib/state_fu/lathe.rb +247 -0
- data/lib/state_fu/logger.rb +10 -0
- data/lib/state_fu/machine.rb +159 -0
- data/lib/state_fu/method_factory.rb +95 -0
- data/lib/state_fu/persistence/active_record.rb +27 -0
- data/lib/state_fu/persistence/attribute.rb +46 -0
- data/lib/state_fu/persistence/base.rb +98 -0
- data/lib/state_fu/persistence/session.rb +7 -0
- data/lib/state_fu/persistence.rb +50 -0
- data/lib/state_fu/sprocket.rb +27 -0
- data/lib/state_fu/state.rb +45 -0
- data/lib/state_fu/transition.rb +213 -0
- data/spec/helper.rb +86 -0
- data/spec/integration/active_record_persistence_spec.rb +189 -0
- data/spec/integration/class_accessor_spec.rb +127 -0
- data/spec/integration/event_definition_spec.rb +74 -0
- data/spec/integration/ex_machine_for_accounts_spec.rb +79 -0
- data/spec/integration/example_01_document_spec.rb +127 -0
- data/spec/integration/example_02_string_spec.rb +87 -0
- data/spec/integration/instance_accessor_spec.rb +100 -0
- data/spec/integration/machine_duplication_spec.rb +95 -0
- data/spec/integration/requirement_reflection_spec.rb +201 -0
- data/spec/integration/sanity_spec.rb +31 -0
- data/spec/integration/state_definition_spec.rb +177 -0
- data/spec/integration/transition_spec.rb +1060 -0
- data/spec/spec.opts +7 -0
- data/spec/units/binding_spec.rb +145 -0
- data/spec/units/event_spec.rb +232 -0
- data/spec/units/exceptions_spec.rb +75 -0
- data/spec/units/fu_space_spec.rb +95 -0
- data/spec/units/lathe_spec.rb +567 -0
- data/spec/units/machine_spec.rb +237 -0
- data/spec/units/method_factory_spec.rb +359 -0
- data/spec/units/sprocket_spec.rb +71 -0
- data/spec/units/state_spec.rb +50 -0
- metadata +122 -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,174 @@
|
|
|
1
|
+
h1. State-Fu
|
|
2
|
+
|
|
3
|
+
h2. What is it?
|
|
4
|
+
|
|
5
|
+
State-Fu is:
|
|
6
|
+
|
|
7
|
+
* an unique toolkit for state-oriented programming
|
|
8
|
+
|
|
9
|
+
* a rich DSL for describing workflows, rules engines and behaviour
|
|
10
|
+
|
|
11
|
+
* something you've probably wanted for a long time but didn't know it
|
|
12
|
+
|
|
13
|
+
It lets you describe:
|
|
14
|
+
|
|
15
|
+
* series of discrete states
|
|
16
|
+
|
|
17
|
+
* events which can change the current state
|
|
18
|
+
|
|
19
|
+
* rules about when these events can occur
|
|
20
|
+
|
|
21
|
+
* behaviours which occur when they do
|
|
22
|
+
|
|
23
|
+
Other libraries exist for ruby which do some or all of these
|
|
24
|
+
things. "What's different about State-Fu?", you may ask.
|
|
25
|
+
|
|
26
|
+
Those libraries you've played with are toys. They're made of
|
|
27
|
+
plastic. State-Fu is forged from a reassuringly dense but
|
|
28
|
+
unidentifiable metal which comes only from the rarest of meteorites,
|
|
29
|
+
and it ticks when you hold it up to your ear.[1]
|
|
30
|
+
|
|
31
|
+
State-Fu is elegant, powerful and transparent enough that you can use
|
|
32
|
+
it to drive substantial parts of your application, and actually want
|
|
33
|
+
to do so.
|
|
34
|
+
|
|
35
|
+
It is designed as a library for authors, as well as users, of
|
|
36
|
+
libraries: State-Fu goes to great lengths to impose very few limits on
|
|
37
|
+
your ability to introspect, manipulate and extend the core features.
|
|
38
|
+
|
|
39
|
+
It is also delightfully elegant and easy to use for simple things:
|
|
40
|
+
|
|
41
|
+
<pre><code>
|
|
42
|
+
|
|
43
|
+
class Document < ActiveRecord::Base
|
|
44
|
+
include StateFu
|
|
45
|
+
|
|
46
|
+
def update_rss
|
|
47
|
+
puts "new feed!"
|
|
48
|
+
# ... do something here
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
machine( :status ) do
|
|
52
|
+
state :draft do
|
|
53
|
+
event :publish, :to => :published
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
state :published do
|
|
57
|
+
on_entry :update_rss
|
|
58
|
+
requires :author # a database column
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
event :delete, :from => :ALL, :to => :deleted do
|
|
62
|
+
execute :destroy
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
my_doc = Document.new
|
|
68
|
+
|
|
69
|
+
my_doc.status # returns a StateFu::Binding, which lets us access the 'Fu
|
|
70
|
+
my_doc.status.state => 'draft' # if this wasn't already a database column or attribute, an
|
|
71
|
+
# attribute has been created to keep track of the state
|
|
72
|
+
my_doc.status.name => :draft # the name of the current_state (defaults to the first defined)
|
|
73
|
+
my_doc.status.publish! # raised => StateFu::RequirementError: [:author]
|
|
74
|
+
# the author requirement prevented the transition
|
|
75
|
+
my_doc.status.name => :draft # see? still a draft.
|
|
76
|
+
my_doc.author = "Susan" # so let's satisfy it ...
|
|
77
|
+
my_doc.publish! # and try again.
|
|
78
|
+
"new feed!" # aha - our event hook fires!
|
|
79
|
+
my_doc.status.name => :published # and the state has been updated.
|
|
80
|
+
|
|
81
|
+
</code></pre>
|
|
82
|
+
|
|
83
|
+
A few of the features which set State-Fu apart for more ambitious work are:
|
|
84
|
+
|
|
85
|
+
* a lovely, simple and flexible API gives you plenty of choices
|
|
86
|
+
|
|
87
|
+
* use an ActiveRecord field for state persistence, or just an
|
|
88
|
+
attribute - or use both, on the same class, for different workflows
|
|
89
|
+
|
|
90
|
+
* customising the persistence mechanism (eg to use a Rails session,
|
|
91
|
+
or a text file) is as easy as defining a getter and setter method
|
|
92
|
+
|
|
93
|
+
* define any number of workflows on the same object / model, or re-use
|
|
94
|
+
them across multiple classes
|
|
95
|
+
|
|
96
|
+
* events can transition from / to any number of states
|
|
97
|
+
|
|
98
|
+
* drive application behaviour with a rich set of event hooks
|
|
99
|
+
|
|
100
|
+
* define behaviour as methods on your objects, or keep it all in the
|
|
101
|
+
state machine itself
|
|
102
|
+
|
|
103
|
+
* requirements determine at runtime whether a particular state
|
|
104
|
+
transition can occur, and if not, can tell a user what they must do
|
|
105
|
+
to satisfy the requirements.
|
|
106
|
+
|
|
107
|
+
* requirement failure messages can be generated at runtime, making
|
|
108
|
+
use of whatever application and state-machine context they need
|
|
109
|
+
|
|
110
|
+
* transitions can be halted mid-execution, and you can actually
|
|
111
|
+
determine why, and where from
|
|
112
|
+
|
|
113
|
+
* in every event hook, requirement filter, and other method calls,
|
|
114
|
+
you have complete and tidy access to your classes, the state
|
|
115
|
+
machine, and the transition context. Use real ruby code anywhere,
|
|
116
|
+
without breathing through a straw!
|
|
117
|
+
|
|
118
|
+
* extend State-Fu with helper modules, or raw blocks of ruby code, to
|
|
119
|
+
model your problem domain - globally, per state machine / workflow,
|
|
120
|
+
or for an individual event transition
|
|
121
|
+
|
|
122
|
+
* store arbitrary meta-data on any component of State-Fu - a simple
|
|
123
|
+
but extremely powerful tool for integration
|
|
124
|
+
|
|
125
|
+
* designed for transparency, introspection and ease of debugging,
|
|
126
|
+
which means a dynamic, powerful system you can actually use without
|
|
127
|
+
headaches
|
|
128
|
+
|
|
129
|
+
* fast, lightweight and useful enough to use in any ruby
|
|
130
|
+
project - works with Rails but does not require it.
|
|
131
|
+
|
|
132
|
+
State-Fu works with any modern Ruby ( 1.8.6, 1.8.7, and 1.9.1)
|
|
133
|
+
|
|
134
|
+
fn1. No disrespect intended to the authors of other similar libraries
|
|
135
|
+
- some of whom I've borrowed an idea or two, and some useful criticism,
|
|
136
|
+
from. They're stand-up guys, all of them. It's the truth though.
|
|
137
|
+
|
|
138
|
+
I'd like to thank Ryan Allen in particular for his Workflow library,
|
|
139
|
+
which I previously forked, piled hundreds of lines of code into and renamed
|
|
140
|
+
Stateful (now deprecated). Some of his ideas (for example the ability to store metadata
|
|
141
|
+
easily on *everything* ) have been instrumental in State-Fu's design.
|
|
142
|
+
|
|
143
|
+
I'd also like to tip my hat at John Barnette, who's own
|
|
144
|
+
(coincidentally named) Stateful set a very high standard with an
|
|
145
|
+
exceptionally elegant API.
|
|
146
|
+
|
|
147
|
+
h2. Getting started
|
|
148
|
+
|
|
149
|
+
First up:
|
|
150
|
+
|
|
151
|
+
<pre>
|
|
152
|
+
<code>
|
|
153
|
+
sudo gem install rspec rr activesupport
|
|
154
|
+
rake
|
|
155
|
+
rake spec:doc # generate specdocs
|
|
156
|
+
rake doc # generate rdoc
|
|
157
|
+
rake gem # build the gem
|
|
158
|
+
rake gem:install # install it
|
|
159
|
+
</code>
|
|
160
|
+
</pre>
|
|
161
|
+
And have a peek in the specs folder or rdoc for usage / documentation.
|
|
162
|
+
|
|
163
|
+
Now you can simply:
|
|
164
|
+
<code>
|
|
165
|
+
require 'state-fu'
|
|
166
|
+
</code>
|
|
167
|
+
and
|
|
168
|
+
<code>
|
|
169
|
+
include StateFu
|
|
170
|
+
</code> in any class you wish to make stateful.
|
|
171
|
+
|
|
172
|
+
If you have questions, feature request or ideas, please join the "google group":http://groups.google.com/group/state-fu
|
|
173
|
+
|
|
174
|
+
Also see the "issue tracker":http://github.com/davidlee/state-fu/issues
|
data/Rakefile
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/ruby1.9
|
|
2
|
+
require "spec/rake/spectask"
|
|
3
|
+
#require 'cucumber/rake/task'
|
|
4
|
+
require "date"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "rubygems"
|
|
7
|
+
|
|
8
|
+
module Rakefile
|
|
9
|
+
def self.windows?
|
|
10
|
+
/djgpp|(cyg|ms|bcc)win|mingw/ =~ RUBY_PLATFORM
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# to build the gem:
|
|
15
|
+
#
|
|
16
|
+
# gem install jeweller
|
|
17
|
+
# rake build
|
|
18
|
+
# rake install
|
|
19
|
+
begin
|
|
20
|
+
require 'jeweler'
|
|
21
|
+
Jeweler::Tasks.new do |s| # gemspec (Gem::Specification)
|
|
22
|
+
s.name = "state-fu"
|
|
23
|
+
s.rubyforge_project = "state-fu"
|
|
24
|
+
s.platform = Gem::Platform::RUBY
|
|
25
|
+
s.has_rdoc = true
|
|
26
|
+
# s.extra_rdoc_files = ["README.rdoc"]
|
|
27
|
+
s.summary = "A rich library for state-oriented programming with state machines / workflows"
|
|
28
|
+
s.description = s.summary
|
|
29
|
+
s.author = "David Lee"
|
|
30
|
+
s.email = "david@rubyist.net.au"
|
|
31
|
+
s.homepage = "http://github.com/davidlee/state-fu"
|
|
32
|
+
s.require_path = "lib"
|
|
33
|
+
# s.files = %w(README.rdoc Rakefile) + Dir.glob("{lib,spec}/**/*")
|
|
34
|
+
s.files = %w(Rakefile) + Dir.glob("{lib,spec}/**/*")
|
|
35
|
+
end
|
|
36
|
+
rescue LoadError
|
|
37
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
namespace :spec do
|
|
41
|
+
desc "Run both units and integration specs"
|
|
42
|
+
task :both => [:units, :integration]
|
|
43
|
+
|
|
44
|
+
desc "Run all specs"
|
|
45
|
+
Spec::Rake::SpecTask.new(:all) do |t|
|
|
46
|
+
t.spec_files = FileList["spec/**/*_spec.rb"]
|
|
47
|
+
t.spec_opts = ["--options", "spec/spec.opts"]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
desc "Run unit specs"
|
|
51
|
+
Spec::Rake::SpecTask.new(:units) do |t|
|
|
52
|
+
t.spec_files = FileList["spec/units/*_spec.rb"]
|
|
53
|
+
t.spec_opts = ["--options", "spec/spec.opts"]
|
|
54
|
+
end
|
|
55
|
+
task :unit => :units
|
|
56
|
+
|
|
57
|
+
desc "Run integration specs"
|
|
58
|
+
Spec::Rake::SpecTask.new(:integration) do |t|
|
|
59
|
+
t.spec_files = FileList["spec/integration/*_spec.rb"]
|
|
60
|
+
t.spec_opts = ["--options", "spec/spec.opts"]
|
|
61
|
+
end
|
|
62
|
+
task :system => :integration
|
|
63
|
+
|
|
64
|
+
desc "Print Specdoc for all specs (eaxcluding plugin specs)"
|
|
65
|
+
Spec::Rake::SpecTask.new(:doc) do |t|
|
|
66
|
+
t.spec_files = FileList["spec/**/*_spec.rb"]
|
|
67
|
+
t.spec_opts = ["--format", "nested","--backtrace","--color"]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
desc "Run autotest"
|
|
71
|
+
task :auto do |t|
|
|
72
|
+
exec 'autospec'
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
desc 'Runs irb in this project\'s context'
|
|
78
|
+
task :irb do |t|
|
|
79
|
+
exec 'irb -I lib -r state-fu'
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
desc 'Runs rdoc on the project lib directory'
|
|
83
|
+
task :doc do |t|
|
|
84
|
+
exec 'rdoc lib/'
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
task :default => 'spec:all'
|
data/lib/no_stdout.rb
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require 'stringio'
|
|
2
|
+
|
|
3
|
+
module NoStdout
|
|
4
|
+
module InstanceMethods
|
|
5
|
+
|
|
6
|
+
def no_stdout ( to = StringIO.new('','r+'), &block )
|
|
7
|
+
# supply an IO of your own to capture STDOUT, otherwise it's put in a StringIO
|
|
8
|
+
orig_stdout = $stdout
|
|
9
|
+
$stdout = @alt_stdout = to
|
|
10
|
+
result = yield
|
|
11
|
+
$stdout = orig_stdout
|
|
12
|
+
result
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def last_stdout
|
|
16
|
+
return nil unless @alt_stdout
|
|
17
|
+
@alt_stdout.rewind
|
|
18
|
+
@alt_stdout.read
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# TODO - explain / remember why this has two class_eval blocks -
|
|
24
|
+
# should one be an extend?
|
|
25
|
+
def self.included klass
|
|
26
|
+
klass.class_eval do
|
|
27
|
+
include InstanceMethods
|
|
28
|
+
end
|
|
29
|
+
klass.extend InstanceMethods
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
end
|
data/lib/state-fu.rb
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
|
|
2
|
+
#!/usr/bin/env ruby
|
|
3
|
+
#
|
|
4
|
+
# State-Fu
|
|
5
|
+
#
|
|
6
|
+
# State-Fu is a framework for state-oriented programming in ruby.
|
|
7
|
+
#
|
|
8
|
+
# You can use it to define state machines, workflows, rules engines,
|
|
9
|
+
# and the behaviours which relate to states and transitions between
|
|
10
|
+
# them.
|
|
11
|
+
#
|
|
12
|
+
# It is powerful and flexible enough to drive entire applications, or
|
|
13
|
+
# substantial parts of them. It is designed as a library for authors,
|
|
14
|
+
# as well as users, of libraries: State-Fu goes to great lengths to
|
|
15
|
+
# impose very few limits on your ability to introspect, manipulate and
|
|
16
|
+
# extend the core features.
|
|
17
|
+
#
|
|
18
|
+
# It is also delightfully elegant and easy to use for simple things:
|
|
19
|
+
#
|
|
20
|
+
# class Document < ActiveRecord::Base
|
|
21
|
+
# include StateFu
|
|
22
|
+
#
|
|
23
|
+
# def update_rss
|
|
24
|
+
# puts "new feed!"
|
|
25
|
+
# # ... do something here
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# machine( :status ) do
|
|
29
|
+
# state :draft do
|
|
30
|
+
# event :publish, :to => :published
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# state :published do
|
|
34
|
+
# on_entry :update_rss
|
|
35
|
+
# requires :author # a database column
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# event :delete, :from => :ALL, :to => :deleted do
|
|
39
|
+
# execute :destroy
|
|
40
|
+
# end
|
|
41
|
+
# end
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# my_doc = Document.new
|
|
45
|
+
#
|
|
46
|
+
# my_doc.status # returns a StateFu::Binding, which lets us access the 'Fu
|
|
47
|
+
# my_doc.status_state => 'draft' # if this wasn't already a database column or attribute, an
|
|
48
|
+
# # attribute has been created to keep track of the state
|
|
49
|
+
# my_doc.status.name => :draft # the name of the current_state (defaults to the first defined)
|
|
50
|
+
# my_doc.status.publish! # raised => StateFu::RequirementError: [:author]
|
|
51
|
+
# # the author requirement prevented the transition
|
|
52
|
+
# my_doc.status.name => :draft # see? still a draft.
|
|
53
|
+
# my_doc.author = "Susan" # so let's satisfy it ...
|
|
54
|
+
# my_doc.publish! # and try again.
|
|
55
|
+
# "new feed!" # aha - our event hook fires!
|
|
56
|
+
# my_doc.status.name => :published # and the state has been updated.
|
|
57
|
+
|
|
58
|
+
require 'activesupport'
|
|
59
|
+
|
|
60
|
+
require 'state_fu/core_ext'
|
|
61
|
+
require 'state_fu/logger'
|
|
62
|
+
require 'state_fu/helper'
|
|
63
|
+
require 'state_fu/exceptions'
|
|
64
|
+
require 'state_fu/fu_space'
|
|
65
|
+
require 'state_fu/machine'
|
|
66
|
+
require 'state_fu/lathe'
|
|
67
|
+
require 'state_fu/method_factory'
|
|
68
|
+
require 'state_fu/binding'
|
|
69
|
+
require 'state_fu/persistence'
|
|
70
|
+
require 'state_fu/persistence/base'
|
|
71
|
+
require 'state_fu/persistence/active_record'
|
|
72
|
+
require 'state_fu/persistence/attribute'
|
|
73
|
+
require 'state_fu/sprocket'
|
|
74
|
+
require 'state_fu/state'
|
|
75
|
+
require 'state_fu/event'
|
|
76
|
+
require 'state_fu/hooks'
|
|
77
|
+
require 'state_fu/interface'
|
|
78
|
+
require 'state_fu/transition'
|
|
79
|
+
|
|
80
|
+
module StateFu
|
|
81
|
+
DEFAULT_MACHINE = :state_fu
|
|
82
|
+
|
|
83
|
+
def self.included( klass )
|
|
84
|
+
klass.extend( Interface::ClassMethods )
|
|
85
|
+
klass.send( :include, Interface::InstanceMethods )
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if __FILE__ == $0
|
|
90
|
+
# run rake stuff (specs / doc )
|
|
91
|
+
# load example_machine.rb
|
|
92
|
+
# drop into irb
|
|
93
|
+
end
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
module StateFu
|
|
2
|
+
class Binding
|
|
3
|
+
include ContextualEval
|
|
4
|
+
|
|
5
|
+
attr_reader :object, :machine, :method_name, :persister, :transitions, :options
|
|
6
|
+
|
|
7
|
+
def initialize( machine, object, method_name, options={} )
|
|
8
|
+
@machine = machine
|
|
9
|
+
@object = object
|
|
10
|
+
@method_name = method_name
|
|
11
|
+
@transitions = []
|
|
12
|
+
@options = options.symbolize_keys!
|
|
13
|
+
field_name = StateFu::FuSpace.field_names[object.class][@method_name]
|
|
14
|
+
raise( ArgumentError, "No field_name" ) unless field_name
|
|
15
|
+
# ensure state field is set up (in case we created this binding
|
|
16
|
+
# manually, instead of via Machine.bind!)
|
|
17
|
+
StateFu::Persistence.prepare_field( object.class, field_name )
|
|
18
|
+
# add a persister
|
|
19
|
+
@persister = StateFu::Persistence.for( self, field_name )
|
|
20
|
+
Logger.info( "Persister added: #@persister ")
|
|
21
|
+
|
|
22
|
+
# define event methods on self( binding ) and @object
|
|
23
|
+
StateFu::MethodFactory.new( self ).install!
|
|
24
|
+
|
|
25
|
+
# StateFu::Persistence.prepare_field( @object.class, field_name )
|
|
26
|
+
end
|
|
27
|
+
alias_method :o, :object
|
|
28
|
+
alias_method :obj, :object
|
|
29
|
+
alias_method :model, :object
|
|
30
|
+
alias_method :instance, :object
|
|
31
|
+
|
|
32
|
+
alias_method :machine, :machine
|
|
33
|
+
alias_method :workflow, :machine
|
|
34
|
+
alias_method :state_machine, :machine
|
|
35
|
+
|
|
36
|
+
def field_name
|
|
37
|
+
persister.field_name
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def current_state
|
|
41
|
+
persister.current_state
|
|
42
|
+
end
|
|
43
|
+
alias_method :at, :current_state
|
|
44
|
+
alias_method :now, :current_state
|
|
45
|
+
alias_method :state, :current_state
|
|
46
|
+
|
|
47
|
+
def current_state_name
|
|
48
|
+
current_state.name
|
|
49
|
+
end
|
|
50
|
+
alias_method :name, :current_state_name
|
|
51
|
+
alias_method :state_name, :current_state_name
|
|
52
|
+
alias_method :to_sym, :current_state_name
|
|
53
|
+
|
|
54
|
+
# a list of events which can fire from the current_state
|
|
55
|
+
def events
|
|
56
|
+
machine.events.select {|e| e.complete? && e.from?( current_state ) }.extend EventArray
|
|
57
|
+
end
|
|
58
|
+
alias_method :events_from_current_state, :events
|
|
59
|
+
|
|
60
|
+
# the subset of events() whose requirements for firing are met
|
|
61
|
+
def valid_events
|
|
62
|
+
return nil unless current_state
|
|
63
|
+
return [] unless current_state.exitable_by?( self )
|
|
64
|
+
events.select {|e| e.fireable_by?( self ) }.extend EventArray
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def invalid_events
|
|
68
|
+
(events - valid_events).extend StateArray
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def unmet_requirements_for(event, target)
|
|
72
|
+
raise NotImplementedError
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# the counterpart to valid_events - the states we can arrive at in
|
|
76
|
+
# the firing of one event, taking into account event and state
|
|
77
|
+
# transition requirements
|
|
78
|
+
def valid_next_states
|
|
79
|
+
valid_transitions.values.flatten.uniq.extend StateArray
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def next_states
|
|
83
|
+
events.map(&:targets).compact.flatten.uniq.extend StateArray
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def invalid_next_states
|
|
87
|
+
states - valid_states
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# returns a hash of { event => [states] } whose transition
|
|
91
|
+
# requirements are met
|
|
92
|
+
def valid_transitions
|
|
93
|
+
h = {}
|
|
94
|
+
return nil if valid_events.nil?
|
|
95
|
+
valid_events.each do |e|
|
|
96
|
+
h[e] = e.targets.select do |s|
|
|
97
|
+
s.enterable_by?( self )
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
h
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# initialize a new transition
|
|
104
|
+
def transition( event_or_array, *args, &block )
|
|
105
|
+
event, target = parse_destination( event_or_array )
|
|
106
|
+
StateFu::Transition.new( self, event, target, *args, &block )
|
|
107
|
+
end
|
|
108
|
+
alias_method :fire, :transition
|
|
109
|
+
alias_method :trigger, :transition
|
|
110
|
+
|
|
111
|
+
# sanitize args for fire! and fireable?
|
|
112
|
+
def parse_destination( event_or_array )
|
|
113
|
+
case event_or_array
|
|
114
|
+
when StateFu::Event, Symbol
|
|
115
|
+
event = event_or_array
|
|
116
|
+
target = nil
|
|
117
|
+
when Array
|
|
118
|
+
event, target = *event_or_array
|
|
119
|
+
end
|
|
120
|
+
x = event_or_array.is_a?( Array ) ? event_or_array.map(&:class) : event_or_array
|
|
121
|
+
raise ArgumentError.new( x.inspect ) unless
|
|
122
|
+
[StateFu::Event, Symbol ].include?( event.class ) &&
|
|
123
|
+
[StateFu::State, Symbol, NilClass].include?( target.class )
|
|
124
|
+
[event, target]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# check that the event and target are "valid" (all requirements met)
|
|
128
|
+
def fireable?( event_or_array )
|
|
129
|
+
event, target = parse_destination( event_or_array )
|
|
130
|
+
begin
|
|
131
|
+
t = transition( [event, target] )
|
|
132
|
+
!! t.requirements_met?
|
|
133
|
+
rescue InvalidTransition => e
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
alias_method :event?, :fireable?
|
|
138
|
+
alias_method :trigger?, :fireable?
|
|
139
|
+
alias_method :triggerable?, :fireable?
|
|
140
|
+
alias_method :transition?, :fireable?
|
|
141
|
+
alias_method :transitionable?,:fireable?
|
|
142
|
+
|
|
143
|
+
# construct an event transition and fire it
|
|
144
|
+
def fire!( event_or_array, *args, &block)
|
|
145
|
+
event, target = parse_destination( event_or_array )
|
|
146
|
+
t = transition( [event, target], *args, &block )
|
|
147
|
+
t.fire!
|
|
148
|
+
t
|
|
149
|
+
end
|
|
150
|
+
alias_method :event!, :fire!
|
|
151
|
+
alias_method :trigger!, :fire!
|
|
152
|
+
alias_method :transition!, :fire!
|
|
153
|
+
|
|
154
|
+
# evaluate a requirement depending whether it's a method or proc,
|
|
155
|
+
# and its arity - see helper.rb (ContextualEval) for the smarts
|
|
156
|
+
def evaluate_requirement( name )
|
|
157
|
+
evaluate_named_proc_or_method( name )
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def evaluate_requirement_message( name, dest )
|
|
161
|
+
msg = machine.requirement_messages[name]
|
|
162
|
+
if [String, NilClass].include?( msg.class )
|
|
163
|
+
return msg
|
|
164
|
+
else
|
|
165
|
+
if dest.is_a?( StateFu::Transition )
|
|
166
|
+
t = dest
|
|
167
|
+
else
|
|
168
|
+
event, target = parse_destination( event_or_array )
|
|
169
|
+
t = transition( event, target )
|
|
170
|
+
end
|
|
171
|
+
case msg
|
|
172
|
+
when Symbol
|
|
173
|
+
t.evaluate_named_proc_or_method( msg )
|
|
174
|
+
when Proc
|
|
175
|
+
t.evaluate &msg
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# if there is one simple event, return a transition for it
|
|
181
|
+
# else return nil
|
|
182
|
+
# TODO - not convinced about the method name / aliases - but next
|
|
183
|
+
# is reserved :/
|
|
184
|
+
def next_transition( *args, &block )
|
|
185
|
+
return nil if valid_transitions.nil?
|
|
186
|
+
next_transition_candidates = valid_transitions.select {|e, s| s.length == 1 }
|
|
187
|
+
if next_transition_candidates.length == 1
|
|
188
|
+
nt = next_transition_candidates.first
|
|
189
|
+
evt = nt[0]
|
|
190
|
+
targ = nt[1][0]
|
|
191
|
+
return transition( [ evt, targ], *args, &block )
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def next_state
|
|
196
|
+
next_transition && next_transition.target
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
alias_method :next_event, :next_transition
|
|
200
|
+
|
|
201
|
+
# if there is a next_transition, create, fire & return it
|
|
202
|
+
# otherwise raise an InvalidTransition
|
|
203
|
+
def next!( *args, &block )
|
|
204
|
+
if t = next_transition( *args, &block )
|
|
205
|
+
t.fire!
|
|
206
|
+
t
|
|
207
|
+
else
|
|
208
|
+
n = valid_transitions && valid_transitions.length
|
|
209
|
+
raise InvalidTransition.
|
|
210
|
+
new( self, current_state, valid_transitions,
|
|
211
|
+
"there are #{n} candidate transitions, need exactly 1")
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
alias_method :next_state!, :next!
|
|
215
|
+
alias_method :next_event!, :next!
|
|
216
|
+
|
|
217
|
+
# if there is a next_transition, return true / false depending on
|
|
218
|
+
# whether its requirements are met
|
|
219
|
+
# otherwise, nil
|
|
220
|
+
def next?( *args, &block )
|
|
221
|
+
if t = next_transition( *args, &block )
|
|
222
|
+
t.requirements_met?
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
alias_method :next_state?, :next?
|
|
226
|
+
alias_method :next_event?, :next?
|
|
227
|
+
|
|
228
|
+
# if there is one possible cyclical event, return a transition there
|
|
229
|
+
def cycle( *args, &block)
|
|
230
|
+
cycle_events = events.select {|e| e.target == current_state }
|
|
231
|
+
if cycle_events.length == 1
|
|
232
|
+
transition( cycle_events[0], *args, &block )
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# if there is a cycle() transition, fire and return it
|
|
237
|
+
# otherwise raise an InvalidTransition
|
|
238
|
+
def cycle!( *args, &block )
|
|
239
|
+
if t = cycle( *args, &block )
|
|
240
|
+
t.fire!
|
|
241
|
+
t
|
|
242
|
+
else
|
|
243
|
+
err_msg = "Cannot cycle! unless there is exactly one event leading from the current state to itself"
|
|
244
|
+
raise InvalidTransition.new( self, current_state, current_state, err_msg )
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# if there is one possible cyclical event, evaluate its
|
|
249
|
+
# requirements (true/false), else nil
|
|
250
|
+
def cycle?
|
|
251
|
+
if t = cycle
|
|
252
|
+
t.requirements_met?
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# display something sensible that doesn't take up the whole screen
|
|
257
|
+
def inspect
|
|
258
|
+
"#<#{self.class} ##{__id__} object_type=#{@object.class} method_name=#{method_name.inspect} field_name=#{persister.field_name.inspect} machine=#{@machine.inspect} options=#{options.inspect}>"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
end
|
|
262
|
+
end
|