davidlee-state-fu 0.2.0 → 0.3.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/README.textile +6 -2
- data/Rakefile +24 -3
- data/lib/no_stdout.rb +28 -5
- data/lib/state-fu.rb +25 -21
- data/lib/state_fu/active_support_lite/misc.rb +57 -0
- data/lib/state_fu/binding.rb +51 -41
- data/lib/state_fu/core_ext.rb +5 -4
- data/lib/state_fu/event.rb +51 -16
- data/lib/state_fu/exceptions.rb +5 -0
- data/lib/state_fu/fu_space.rb +5 -4
- data/lib/state_fu/helper.rb +25 -3
- data/lib/state_fu/hooks.rb +4 -1
- data/lib/state_fu/interface.rb +20 -24
- data/lib/state_fu/lathe.rb +38 -2
- data/lib/state_fu/logger.rb +84 -6
- data/lib/state_fu/machine.rb +3 -0
- data/lib/state_fu/method_factory.rb +3 -3
- data/lib/state_fu/persistence/active_record.rb +3 -1
- data/lib/state_fu/persistence/attribute.rb +4 -4
- data/lib/state_fu/persistence/base.rb +3 -3
- data/lib/state_fu/persistence/relaxdb.rb +23 -0
- data/lib/state_fu/persistence.rb +24 -29
- data/lib/state_fu/plotter.rb +63 -0
- data/lib/state_fu/sprocket.rb +12 -0
- data/lib/state_fu/state.rb +22 -0
- data/lib/state_fu/transition.rb +13 -0
- data/lib/vizier.rb +300 -0
- data/spec/BDD/plotter_spec.rb +115 -0
- data/spec/features/binding_and_transition_helper_mixin_spec.rb +111 -0
- data/spec/features/not_requirements_spec.rb +81 -0
- data/spec/features/state_and_array_options_accessor_spec.rb +47 -0
- data/spec/features/transition_boolean_comparison.rb +90 -0
- data/spec/helper.rb +33 -0
- data/spec/integration/active_record_persistence_spec.rb +0 -1
- data/spec/integration/example_01_document_spec.rb +1 -1
- data/spec/integration/relaxdb_persistence_spec.rb +94 -0
- data/spec/integration/requirement_reflection_spec.rb +2 -2
- data/spec/integration/transition_spec.rb +9 -1
- data/spec/units/binding_spec.rb +46 -17
- data/spec/units/lathe_spec.rb +11 -10
- data/spec/units/method_factory_spec.rb +6 -1
- metadata +37 -23
- data/spec/integration/temp_spec.rb +0 -17
data/README.textile
CHANGED
|
@@ -126,6 +126,8 @@ A few of the features which set State-Fu apart for more ambitious work are:
|
|
|
126
126
|
which means a dynamic, powerful system you can actually use without
|
|
127
127
|
headaches
|
|
128
128
|
|
|
129
|
+
* magically generate diagrams of state machines / workflows with graphviz
|
|
130
|
+
|
|
129
131
|
* fast, lightweight and useful enough to use in any ruby
|
|
130
132
|
project - works with Rails but does not require it.
|
|
131
133
|
|
|
@@ -194,7 +196,9 @@ ActiveSupport (e.g. ActiveRecord), you may have to explicitly
|
|
|
194
196
|
<code>require 'activesupport'</code> before loading the dependent
|
|
195
197
|
libraries.
|
|
196
198
|
|
|
197
|
-
|
|
198
199
|
Also see the "issue tracker":http://github.com/davidlee/state-fu/issues
|
|
199
200
|
|
|
200
|
-
And the "build monitor":http://runcoderun.com/davidlee/state-fu/
|
|
201
|
+
And the "build monitor":http://runcoderun.com/davidlee/state-fu/
|
|
202
|
+
|
|
203
|
+
And the "RDoc":http://rdoc.info/projects/davidlee/state-fu/ , which
|
|
204
|
+
needs a bit of love.
|
data/Rakefile
CHANGED
|
@@ -38,9 +38,6 @@ rescue LoadError
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
namespace :spec do
|
|
41
|
-
desc "Run both units and integration specs"
|
|
42
|
-
task :both => [:units, :integration]
|
|
43
|
-
|
|
44
41
|
desc "Run all specs"
|
|
45
42
|
Spec::Rake::SpecTask.new(:all) do |t|
|
|
46
43
|
t.spec_files = FileList["spec/**/*_spec.rb"]
|
|
@@ -72,6 +69,21 @@ namespace :spec do
|
|
|
72
69
|
exec 'autospec'
|
|
73
70
|
end
|
|
74
71
|
|
|
72
|
+
def find_last_modified_spec
|
|
73
|
+
require 'find'
|
|
74
|
+
specs = []
|
|
75
|
+
Find.find( File.expand_path(File.join(File.dirname(__FILE__),'spec'))) do |f|
|
|
76
|
+
next unless f !~ /\.#/ && f =~ /_spec.rb$/
|
|
77
|
+
specs << f
|
|
78
|
+
end
|
|
79
|
+
spec = specs.sort_by { |spec| File.stat( spec ).mtime }.last
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
desc "runs the last modified spec, without mucking about"
|
|
83
|
+
Spec::Rake::SpecTask.new(:last) do |t|
|
|
84
|
+
t.spec_opts = ['--options', "\"#{File.dirname(__FILE__)}/spec/spec.opts\""]
|
|
85
|
+
t.spec_files = FileList[find_last_modified_spec]
|
|
86
|
+
end
|
|
75
87
|
end
|
|
76
88
|
|
|
77
89
|
desc 'Runs irb in this project\'s context'
|
|
@@ -84,4 +96,13 @@ task :doc do |t|
|
|
|
84
96
|
exec 'rdoc lib/'
|
|
85
97
|
end
|
|
86
98
|
|
|
99
|
+
begin
|
|
100
|
+
require 'cucumber/rake/task'
|
|
101
|
+
|
|
102
|
+
Cucumber::Rake::Task.new(:features) do |t|
|
|
103
|
+
t.cucumber_opts = "--format pretty"
|
|
104
|
+
end
|
|
105
|
+
rescue LoadError => e
|
|
106
|
+
end
|
|
107
|
+
|
|
87
108
|
task :default => 'spec:all'
|
data/lib/no_stdout.rb
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
require 'stringio'
|
|
2
2
|
|
|
3
|
+
# a module for suppressing or capturing STDOUT or STDERR.
|
|
4
|
+
# useful when shelling out to "noisy" applications or to suppress
|
|
5
|
+
# output during tests.
|
|
3
6
|
module NoStdout
|
|
4
7
|
module InstanceMethods
|
|
5
8
|
|
|
9
|
+
# Suppresses or redirects STDOUT inside the given block.
|
|
10
|
+
# supply an IO of your own to capture STDOUT, otherwise it's put
|
|
11
|
+
# in a new StringIO object.
|
|
6
12
|
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
13
|
orig_stdout = $stdout
|
|
9
14
|
$stdout = @alt_stdout = to
|
|
10
15
|
result = yield
|
|
@@ -12,21 +17,39 @@ module NoStdout
|
|
|
12
17
|
result
|
|
13
18
|
end
|
|
14
19
|
|
|
20
|
+
# returns the contents of STDOUT from the previous usage of
|
|
21
|
+
# no_stdout, or nil
|
|
15
22
|
def last_stdout
|
|
16
23
|
return nil unless @alt_stdout
|
|
17
24
|
@alt_stdout.rewind
|
|
18
25
|
@alt_stdout.read
|
|
19
26
|
end
|
|
20
27
|
|
|
28
|
+
## COPIED FROM ABOVE ####
|
|
29
|
+
|
|
30
|
+
# Suppresses or redirects STDERR inside the given block.
|
|
31
|
+
# supply an IO of your own to capture STDERR, otherwise it's put
|
|
32
|
+
# in a new StringIO object.
|
|
33
|
+
def no_stderr ( to = StringIO.new('','r+'), &block )
|
|
34
|
+
orig_stderr = $stderr
|
|
35
|
+
$stderr = @alt_stderr = to
|
|
36
|
+
result = yield
|
|
37
|
+
$stderr = orig_stderr
|
|
38
|
+
result
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# returns the contents of STDERR from the previous usage of
|
|
42
|
+
# no_stderr, or nil
|
|
43
|
+
def last_stderr
|
|
44
|
+
return nil unless @alt_stderr
|
|
45
|
+
@alt_stderr.rewind
|
|
46
|
+
@alt_stderr.read
|
|
47
|
+
end
|
|
21
48
|
end
|
|
22
49
|
|
|
23
|
-
# TODO - explain / remember why this has two class_eval blocks -
|
|
24
|
-
# should one be an extend?
|
|
25
50
|
def self.included klass
|
|
26
51
|
klass.class_eval do
|
|
27
52
|
include InstanceMethods
|
|
28
53
|
end
|
|
29
|
-
klass.extend InstanceMethods
|
|
30
54
|
end
|
|
31
|
-
|
|
32
55
|
end
|
data/lib/state-fu.rb
CHANGED
|
@@ -50,33 +50,37 @@
|
|
|
50
50
|
# # the author requirement prevented the transition
|
|
51
51
|
# my_doc.status.name => :draft # see? still a draft.
|
|
52
52
|
# my_doc.author = "Susan" # so let's satisfy it ...
|
|
53
|
-
# my_doc.publish! # and try again
|
|
53
|
+
# my_doc.publish! # and try again
|
|
54
54
|
# "new feed!" # aha - our event hook fires!
|
|
55
55
|
# my_doc.status.name => :published # and the state has been updated.
|
|
56
56
|
|
|
57
57
|
require 'rubygems'
|
|
58
58
|
# require 'activesupport'
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
60
|
+
[ 'core_ext',
|
|
61
|
+
'logger',
|
|
62
|
+
'helper',
|
|
63
|
+
'exceptions',
|
|
64
|
+
'fu_space',
|
|
65
|
+
'machine',
|
|
66
|
+
'lathe',
|
|
67
|
+
'method_factory',
|
|
68
|
+
'binding',
|
|
69
|
+
'persistence',
|
|
70
|
+
'persistence/base',
|
|
71
|
+
'persistence/active_record',
|
|
72
|
+
'persistence/attribute',
|
|
73
|
+
'persistence/relaxdb',
|
|
74
|
+
'sprocket',
|
|
75
|
+
'state',
|
|
76
|
+
'event',
|
|
77
|
+
'hooks',
|
|
78
|
+
'interface',
|
|
79
|
+
'transition',
|
|
80
|
+
'mock_transition',
|
|
81
|
+
'plotter' ].each do |lib|
|
|
82
|
+
require File.expand_path( File.join( File.dirname(__FILE__), 'state_fu', lib ))
|
|
83
|
+
end
|
|
80
84
|
|
|
81
85
|
module StateFu
|
|
82
86
|
DEFAULT_MACHINE = :state_fu
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
class Object
|
|
2
|
+
# Returns +value+ after yielding +value+ to the block. This simplifies the
|
|
3
|
+
# process of constructing an object, performing work on the object, and then
|
|
4
|
+
# returning the object from a method. It is a Ruby-ized realization of the K
|
|
5
|
+
# combinator, courtesy of Mikael Brockman.
|
|
6
|
+
#
|
|
7
|
+
# ==== Examples
|
|
8
|
+
#
|
|
9
|
+
# # Without returning
|
|
10
|
+
# def foo
|
|
11
|
+
# values = []
|
|
12
|
+
# values << "bar"
|
|
13
|
+
# values << "baz"
|
|
14
|
+
# return values
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# foo # => ['bar', 'baz']
|
|
18
|
+
#
|
|
19
|
+
# # returning with a local variable
|
|
20
|
+
# def foo
|
|
21
|
+
# returning values = [] do
|
|
22
|
+
# values << 'bar'
|
|
23
|
+
# values << 'baz'
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# foo # => ['bar', 'baz']
|
|
28
|
+
#
|
|
29
|
+
# # returning with a block argument
|
|
30
|
+
# def foo
|
|
31
|
+
# returning [] do |values|
|
|
32
|
+
# values << 'bar'
|
|
33
|
+
# values << 'baz'
|
|
34
|
+
# end
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# foo # => ['bar', 'baz']
|
|
38
|
+
def returning(value)
|
|
39
|
+
yield(value)
|
|
40
|
+
value
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Yields <code>x</code> to the block, and then returns <code>x</code>.
|
|
44
|
+
# The primary purpose of this method is to "tap into" a method chain,
|
|
45
|
+
# in order to perform operations on intermediate results within the chain.
|
|
46
|
+
#
|
|
47
|
+
# (1..10).tap { |x| puts "original: #{x.inspect}" }.to_a.
|
|
48
|
+
# tap { |x| puts "array: #{x.inspect}" }.
|
|
49
|
+
# select { |x| x%2 == 0 }.
|
|
50
|
+
# tap { |x| puts "evens: #{x.inspect}" }.
|
|
51
|
+
# map { |x| x*x }.
|
|
52
|
+
# tap { |x| puts "squares: #{x.inspect}" }
|
|
53
|
+
def tap
|
|
54
|
+
yield self
|
|
55
|
+
self
|
|
56
|
+
end unless Object.respond_to?(:tap)
|
|
57
|
+
end
|
data/lib/state_fu/binding.rb
CHANGED
|
@@ -4,6 +4,14 @@ module StateFu
|
|
|
4
4
|
|
|
5
5
|
attr_reader :object, :machine, :method_name, :persister, :transitions, :options
|
|
6
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
|
+
#
|
|
7
15
|
def initialize( machine, object, method_name, options={} )
|
|
8
16
|
@machine = machine
|
|
9
17
|
@object = object
|
|
@@ -17,7 +25,7 @@ module StateFu
|
|
|
17
25
|
StateFu::Persistence.prepare_field( object.class, field_name )
|
|
18
26
|
# add a persister
|
|
19
27
|
@persister = StateFu::Persistence.for( self, field_name )
|
|
20
|
-
Logger.
|
|
28
|
+
Logger.debug( "Persister (#{@persister.class}) added: #{method_name} as field #{field_name}" )
|
|
21
29
|
|
|
22
30
|
# define event methods on self( binding ) and @object
|
|
23
31
|
StateFu::MethodFactory.new( self ).install!
|
|
@@ -33,10 +41,12 @@ module StateFu
|
|
|
33
41
|
alias_method :workflow, :machine
|
|
34
42
|
alias_method :state_machine, :machine
|
|
35
43
|
|
|
44
|
+
# the perister's field_name (a symbol)
|
|
36
45
|
def field_name
|
|
37
46
|
persister.field_name
|
|
38
47
|
end
|
|
39
48
|
|
|
49
|
+
# the current_state, as maintained by the persister.
|
|
40
50
|
def current_state
|
|
41
51
|
persister.current_state
|
|
42
52
|
end
|
|
@@ -44,6 +54,7 @@ module StateFu
|
|
|
44
54
|
alias_method :now, :current_state
|
|
45
55
|
alias_method :state, :current_state
|
|
46
56
|
|
|
57
|
+
# the name, as a Symbol, of the binding's current_state
|
|
47
58
|
def current_state_name
|
|
48
59
|
begin
|
|
49
60
|
current_state.name.to_sym
|
|
@@ -55,19 +66,22 @@ module StateFu
|
|
|
55
66
|
alias_method :state_name, :current_state_name
|
|
56
67
|
alias_method :to_sym, :current_state_name
|
|
57
68
|
|
|
58
|
-
# a list of
|
|
69
|
+
# returns a list of StateFu::Events which can fire from the current_state
|
|
59
70
|
def events
|
|
60
71
|
machine.events.select {|e| e.complete? && e.from?( current_state ) }.extend EventArray
|
|
61
72
|
end
|
|
62
73
|
alias_method :events_from_current_state, :events
|
|
63
74
|
|
|
64
75
|
# the subset of events() whose requirements for firing are met
|
|
76
|
+
# (with the arguments supplied, if any)
|
|
65
77
|
def valid_events( *args )
|
|
66
78
|
return nil unless current_state
|
|
67
79
|
return [] unless current_state.exitable_by?( self, *args )
|
|
68
80
|
events.select {|e| e.fireable_by?( self, *args ) }.extend EventArray
|
|
69
81
|
end
|
|
70
82
|
|
|
83
|
+
# the subset of events() whose requirements for firing are NOT met
|
|
84
|
+
# (with the arguments supplied, if any)
|
|
71
85
|
def invalid_events( *args )
|
|
72
86
|
( events - valid_events( *args ) ).extend StateArray
|
|
73
87
|
end
|
|
@@ -84,6 +98,7 @@ module StateFu
|
|
|
84
98
|
vt && vt.values.flatten.uniq.extend( StateArray )
|
|
85
99
|
end
|
|
86
100
|
|
|
101
|
+
#
|
|
87
102
|
def next_states
|
|
88
103
|
events.map(&:targets).compact.flatten.uniq.extend StateArray
|
|
89
104
|
end
|
|
@@ -101,7 +116,11 @@ module StateFu
|
|
|
101
116
|
h
|
|
102
117
|
end
|
|
103
118
|
|
|
104
|
-
#
|
|
119
|
+
# initializes a new Transition to the given destination, with the
|
|
120
|
+
# given *args (to be passed to requirements and hooks).
|
|
121
|
+
#
|
|
122
|
+
# If a block is given, it yields the Transition or is executed in
|
|
123
|
+
# its evaluation context, depending on the arity of the block.
|
|
105
124
|
def transition( event_or_array, *args, &block )
|
|
106
125
|
event, target = parse_destination( event_or_array )
|
|
107
126
|
StateFu::Transition.new( self, event, target, *args, &block )
|
|
@@ -112,17 +131,24 @@ module StateFu
|
|
|
112
131
|
alias_method :trigger_event, :transition
|
|
113
132
|
alias_method :begin_transition, :transition
|
|
114
133
|
|
|
134
|
+
# return a MockTransition to nowhere and passes it the given
|
|
135
|
+
# *args. Useful for evaluating requirements in spec / test code.
|
|
115
136
|
def blank_mock_transition( *args, &block )
|
|
116
137
|
StateFu::MockTransition.new( self, nil, nil, *args, &block )
|
|
117
138
|
end
|
|
118
139
|
|
|
140
|
+
# return a MockTransition; otherwise the same as #transition
|
|
119
141
|
def mock_transition( event_or_array, *args, &block )
|
|
120
142
|
event, target = nil
|
|
121
143
|
event, target = parse_destination( event_or_array )
|
|
122
144
|
StateFu::MockTransition.new( self, event, target, *args, &block )
|
|
123
145
|
end
|
|
124
146
|
|
|
125
|
-
#
|
|
147
|
+
# sanitizes / extracts destination from *args for other methods.
|
|
148
|
+
#
|
|
149
|
+
# takes a single, simple (one target only) event,
|
|
150
|
+
# or an array of [event, target],
|
|
151
|
+
# or one of the above with symbols in place of the objects themselves.
|
|
126
152
|
def parse_destination( event_or_array )
|
|
127
153
|
case event_or_array
|
|
128
154
|
when StateFu::Event, Symbol
|
|
@@ -138,7 +164,8 @@ module StateFu
|
|
|
138
164
|
[event, target]
|
|
139
165
|
end
|
|
140
166
|
|
|
141
|
-
# check that the event and target are
|
|
167
|
+
# check that the event and target are valid (all requirements are
|
|
168
|
+
# met) with the given (optional) arguments
|
|
142
169
|
def fireable?( event_or_array, *args )
|
|
143
170
|
event, target = parse_destination( event_or_array )
|
|
144
171
|
begin
|
|
@@ -176,47 +203,17 @@ module StateFu
|
|
|
176
203
|
# and its arity - see helper.rb (ContextualEval) for the smarts
|
|
177
204
|
|
|
178
205
|
# TODO - enable requirement block / method to know the target
|
|
179
|
-
def evaluate_requirement( name )
|
|
180
|
-
puts "DEPRECATED: evaluate_requirement #{name}"
|
|
181
|
-
evaluate_requirement_with_args( name )
|
|
182
|
-
end
|
|
183
206
|
|
|
184
207
|
def evaluate_requirement_with_args( name, *args )
|
|
185
208
|
t = blank_mock_transition( *args )
|
|
186
209
|
evaluate_named_proc_or_method( name, t )
|
|
187
210
|
end
|
|
211
|
+
# alias_method :evaluate_requirement, :evaluate_requirement_with_args
|
|
188
212
|
|
|
189
|
-
|
|
190
|
-
evaluate_named_proc_or_method( name, t )
|
|
191
|
-
end
|
|
213
|
+
alias_method :evaluate_requirement_with_transition, :evaluate_named_proc_or_method
|
|
192
214
|
|
|
193
|
-
#
|
|
194
|
-
#
|
|
195
|
-
# # puts "#{name} #{dest}"
|
|
196
|
-
# msg = machine.requirement_messages[name]
|
|
197
|
-
# if [String, NilClass].include?( msg.class )
|
|
198
|
-
# return msg
|
|
199
|
-
# else
|
|
200
|
-
# if dest.is_a?( StateFu::Transition )
|
|
201
|
-
# t = dest
|
|
202
|
-
# else
|
|
203
|
-
# event, target = parse_destination( event_or_array )
|
|
204
|
-
# t = transition( event, target )
|
|
205
|
-
# end
|
|
206
|
-
# case msg
|
|
207
|
-
# when Symbol, Proc
|
|
208
|
-
# puts t.class
|
|
209
|
-
# evaluate_named_proc_or_method( msg, t )
|
|
210
|
-
# # when Proc
|
|
211
|
-
# # t.evaluate &msg
|
|
212
|
-
# end
|
|
213
|
-
# end
|
|
214
|
-
# end
|
|
215
|
-
|
|
216
|
-
# if there is one simple event, return a transition for it
|
|
217
|
-
# else return nil
|
|
218
|
-
# TODO - not convinced about the method name / aliases - but next
|
|
219
|
-
# is reserved :/
|
|
215
|
+
# if there is exactly one legal transition which can be fired with
|
|
216
|
+
# the given (optional) arguments, return it.
|
|
220
217
|
def next_transition( *args, &block )
|
|
221
218
|
vts = valid_transitions( *args )
|
|
222
219
|
return nil if vts.nil?
|
|
@@ -229,11 +226,15 @@ module StateFu
|
|
|
229
226
|
end
|
|
230
227
|
end
|
|
231
228
|
|
|
229
|
+
# if there is exactly one state reachable via a transition which
|
|
230
|
+
# is valid with the given optional arguments, return it.
|
|
232
231
|
def next_state( *args )
|
|
233
232
|
nt = next_transition( *args )
|
|
234
233
|
nt && nt.target
|
|
235
234
|
end
|
|
236
235
|
|
|
236
|
+
# if there is exactly one event which is valid with the given
|
|
237
|
+
# optional arguments, return it
|
|
237
238
|
def next_event( *args )
|
|
238
239
|
nt = next_transition( *args )
|
|
239
240
|
nt && nt.event
|
|
@@ -294,6 +295,13 @@ module StateFu
|
|
|
294
295
|
end
|
|
295
296
|
end
|
|
296
297
|
|
|
298
|
+
# change the current state of the binding without any
|
|
299
|
+
# requirements or other sanity checks, or any hooks firing.
|
|
300
|
+
# Useful for test / spec scenarios, and abusing the framework.
|
|
301
|
+
def teleport!( target )
|
|
302
|
+
persister.current_state=( machine.states[target] )
|
|
303
|
+
end
|
|
304
|
+
|
|
297
305
|
# display something sensible that doesn't take up the whole screen
|
|
298
306
|
def inspect
|
|
299
307
|
'|<= ' + self.class.to_s + ' ' +
|
|
@@ -305,9 +313,11 @@ module StateFu
|
|
|
305
313
|
map {|x| x.join('=') }.join( " " ) + ' =>|'
|
|
306
314
|
end
|
|
307
315
|
|
|
316
|
+
# let's be == the current_state_name as a symbol.
|
|
317
|
+
# a nice little convenience.
|
|
308
318
|
def == other
|
|
309
|
-
if other.respond_to?(:to_sym) &&
|
|
310
|
-
other.to_sym
|
|
319
|
+
if other.respond_to?( :to_sym ) && current_state
|
|
320
|
+
current_state_name == other.to_sym || super( other )
|
|
311
321
|
else
|
|
312
322
|
super( other )
|
|
313
323
|
end
|
data/lib/state_fu/core_ext.rb
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
require 'rubygems'
|
|
2
|
-
|
|
3
|
-
class Symbol
|
|
2
|
+
# ruby1.9 style symbol comparability for ruby1.8
|
|
3
|
+
class Symbol # :nodoc:
|
|
4
4
|
unless instance_methods.include?(:'<=>')
|
|
5
|
-
# Logger.log ..
|
|
6
5
|
def <=> other
|
|
7
6
|
self.to_s <=> other.to_s
|
|
8
7
|
end
|
|
9
8
|
end
|
|
10
9
|
end
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
# if ActiveSupport is absent, install a very small subset of it for
|
|
12
|
+
# some convenience methods
|
|
13
|
+
unless Object.const_defined?('ActiveSupport') # :nodoc:
|
|
13
14
|
Dir[File.join(File.dirname( __FILE__), 'active_support_lite','**' )].sort.each do |lib|
|
|
14
15
|
next unless File.file?( lib )
|
|
15
16
|
require lib
|
data/lib/state_fu/event.rb
CHANGED
|
@@ -3,67 +3,93 @@ module StateFu
|
|
|
3
3
|
|
|
4
4
|
attr_reader :origins, :targets, :requirements
|
|
5
5
|
|
|
6
|
-
#
|
|
7
|
-
# TODO - event guards
|
|
8
|
-
#
|
|
9
|
-
|
|
6
|
+
# called by Lathe when a new event is constructed
|
|
10
7
|
def initialize(machine, name, options={})
|
|
11
8
|
@requirements = [].extend ArrayWithSymbolAccessor
|
|
12
9
|
super( machine, name, options )
|
|
13
10
|
end
|
|
14
11
|
|
|
12
|
+
# the names of all possible origin states
|
|
15
13
|
def origin_names
|
|
16
14
|
origins ? origins.map(&:to_sym) : nil
|
|
17
15
|
end
|
|
18
16
|
|
|
17
|
+
# the names of all possible target states
|
|
19
18
|
def target_names
|
|
20
19
|
targets ? targets.map(&:to_sym) : nil
|
|
21
20
|
end
|
|
22
21
|
|
|
22
|
+
# tests if a state or state name is in the list of targets
|
|
23
23
|
def to?( state )
|
|
24
24
|
target_names.include?( state.to_sym )
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# tests if a state or state name is in the list of origins
|
|
27
28
|
def from?( state )
|
|
28
29
|
origin_names.include?( state.to_sym )
|
|
29
30
|
end
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
# internal method which accumulates states into an instance
|
|
33
|
+
# variable with successive invocations.
|
|
34
|
+
# ensures that calling #from multiple times adds to, rather than
|
|
35
|
+
# clobbering, the list of origins / targets.
|
|
36
|
+
def update_state_collection( ivar_name, *args)
|
|
37
|
+
new_states = if [args].flatten == [:ALL]
|
|
38
|
+
machine.states
|
|
39
|
+
else
|
|
40
|
+
machine.find_or_create_states_by_name( *args.flatten )
|
|
41
|
+
end
|
|
42
|
+
unless new_states.is_a?( Array )
|
|
43
|
+
new_states = [new_states]
|
|
36
44
|
end
|
|
45
|
+
existing = instance_variable_get( ivar_name )
|
|
46
|
+
# return existing if new_states.empty?
|
|
47
|
+
new_value = ((existing || [] ) + new_states).flatten.compact.uniq.extend( StateArray )
|
|
48
|
+
instance_variable_set( ivar_name, new_value )
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# *adds to* the origin states given a list of symbols / States
|
|
52
|
+
def origins=( *args )
|
|
53
|
+
update_state_collection( '@origins', *args )
|
|
37
54
|
end
|
|
38
55
|
|
|
56
|
+
# *adds to* the target states given a list of symbols / States
|
|
39
57
|
def targets=( *args )
|
|
40
|
-
|
|
41
|
-
@targets = machine.states
|
|
42
|
-
else
|
|
43
|
-
@targets = machine.find_or_create_states_by_name( *args.flatten ).extend( StateArray )
|
|
44
|
-
end
|
|
58
|
+
update_state_collection( '@targets', *args )
|
|
45
59
|
end
|
|
46
60
|
|
|
47
|
-
|
|
48
|
-
#
|
|
61
|
+
|
|
62
|
+
# used internally
|
|
63
|
+
#
|
|
64
|
+
# <tt>complete?(:origins) # do we have origins?<tt>
|
|
65
|
+
# <tt>complete? # do we have origins and targets?<tt>
|
|
49
66
|
def complete?( field = nil )
|
|
50
67
|
( field && [field] || [:origins, :targets] ).
|
|
51
68
|
map{ |s| send(s) }.
|
|
52
69
|
all?{ |f| !(f.nil? || f.empty?) }
|
|
53
70
|
end
|
|
54
71
|
|
|
72
|
+
# if there is a single state in #origins, returns it
|
|
55
73
|
def origin
|
|
56
74
|
origins && origins.length == 1 && origins[0] || nil
|
|
57
75
|
end
|
|
58
76
|
|
|
77
|
+
# if there is a single state in #origins, returns it
|
|
59
78
|
def target
|
|
60
79
|
targets && targets.length == 1 && targets[0] || nil
|
|
61
80
|
end
|
|
62
81
|
|
|
82
|
+
# a simple event has exactly one target, and any number of
|
|
83
|
+
# origins. It's simple because it can be triggered without
|
|
84
|
+
# supplying a target name - ie, <tt>go!<tt> vs <tt>go!(:home)<tt>
|
|
63
85
|
def simple?
|
|
64
86
|
!! ( origins && target )
|
|
65
87
|
end
|
|
66
88
|
|
|
89
|
+
# generally called from a Lathe. Sets the origin(s) and optionally
|
|
90
|
+
# target(s) - that is, if you supply the :to option, or a single element
|
|
91
|
+
# hash of origins => targets ) of the event. Both origins= and
|
|
92
|
+
# targets= are accumulators.
|
|
67
93
|
def from *args
|
|
68
94
|
options = args.extract_options!.symbolize_keys!
|
|
69
95
|
args.flatten!
|
|
@@ -81,6 +107,7 @@ module StateFu
|
|
|
81
107
|
end
|
|
82
108
|
end
|
|
83
109
|
|
|
110
|
+
# sets the target states for the event.
|
|
84
111
|
def to *args
|
|
85
112
|
options = args.extract_options!.symbolize_keys!
|
|
86
113
|
args.flatten!
|
|
@@ -88,11 +115,19 @@ module StateFu
|
|
|
88
115
|
self.targets= *args
|
|
89
116
|
end
|
|
90
117
|
|
|
118
|
+
# is the event legal for the given binding, with the given
|
|
119
|
+
# (optional) arguments?
|
|
91
120
|
def fireable_by?( binding, *args )
|
|
92
121
|
requirements.reject do |r|
|
|
93
122
|
binding.evaluate_requirement_with_args( r, *args )
|
|
94
123
|
end.empty?
|
|
95
124
|
end
|
|
96
125
|
|
|
126
|
+
# adds an event requirement.
|
|
127
|
+
# TODO MOREDOC
|
|
128
|
+
def requires( *args, &block )
|
|
129
|
+
lathe.requires( *args, &block )
|
|
130
|
+
end
|
|
131
|
+
|
|
97
132
|
end
|
|
98
133
|
end
|
data/lib/state_fu/exceptions.rb
CHANGED
|
@@ -8,6 +8,11 @@ module StateFu
|
|
|
8
8
|
attr_reader :transition
|
|
9
9
|
DEFAULT_MESSAGE = "The transition was halted"
|
|
10
10
|
|
|
11
|
+
# SPECME
|
|
12
|
+
def unmet_requirements
|
|
13
|
+
transition.unmet_requirements
|
|
14
|
+
end
|
|
15
|
+
|
|
11
16
|
def initialize( transition, message=DEFAULT_MESSAGE, options={})
|
|
12
17
|
@transition = transition
|
|
13
18
|
@options = options
|
data/lib/state_fu/fu_space.rb
CHANGED
|
@@ -5,11 +5,12 @@ module StateFu
|
|
|
5
5
|
class FuSpace
|
|
6
6
|
cattr_reader :named_machines, :class_machines, :field_names
|
|
7
7
|
|
|
8
|
-
# class_machines[ Class ][ method_name ] # => a StateFu::Machine
|
|
9
|
-
# class_machines[ Klass ][ nil ] # => the Klass's default Machine
|
|
10
|
-
# field_names[ Class ][ method_name ] # => name of attribute / db field
|
|
11
|
-
|
|
12
8
|
# return the default machine, or an empty hash, given a missing index.
|
|
9
|
+
#
|
|
10
|
+
# * class_machines[ Class ][ method_name ] # => a StateFu::Machine
|
|
11
|
+
# * class_machines[ Klass ][ nil ] # => the Klass's default Machine
|
|
12
|
+
# * field_names[ Class ][ method_name ] # => name of attribute / db field
|
|
13
|
+
#
|
|
13
14
|
LAZY_HASH = lambda do |h, k|
|
|
14
15
|
if k.nil?
|
|
15
16
|
self[ StateFu::DEFAULT_MACHINE ]
|