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.
Files changed (43) hide show
  1. data/README.textile +6 -2
  2. data/Rakefile +24 -3
  3. data/lib/no_stdout.rb +28 -5
  4. data/lib/state-fu.rb +25 -21
  5. data/lib/state_fu/active_support_lite/misc.rb +57 -0
  6. data/lib/state_fu/binding.rb +51 -41
  7. data/lib/state_fu/core_ext.rb +5 -4
  8. data/lib/state_fu/event.rb +51 -16
  9. data/lib/state_fu/exceptions.rb +5 -0
  10. data/lib/state_fu/fu_space.rb +5 -4
  11. data/lib/state_fu/helper.rb +25 -3
  12. data/lib/state_fu/hooks.rb +4 -1
  13. data/lib/state_fu/interface.rb +20 -24
  14. data/lib/state_fu/lathe.rb +38 -2
  15. data/lib/state_fu/logger.rb +84 -6
  16. data/lib/state_fu/machine.rb +3 -0
  17. data/lib/state_fu/method_factory.rb +3 -3
  18. data/lib/state_fu/persistence/active_record.rb +3 -1
  19. data/lib/state_fu/persistence/attribute.rb +4 -4
  20. data/lib/state_fu/persistence/base.rb +3 -3
  21. data/lib/state_fu/persistence/relaxdb.rb +23 -0
  22. data/lib/state_fu/persistence.rb +24 -29
  23. data/lib/state_fu/plotter.rb +63 -0
  24. data/lib/state_fu/sprocket.rb +12 -0
  25. data/lib/state_fu/state.rb +22 -0
  26. data/lib/state_fu/transition.rb +13 -0
  27. data/lib/vizier.rb +300 -0
  28. data/spec/BDD/plotter_spec.rb +115 -0
  29. data/spec/features/binding_and_transition_helper_mixin_spec.rb +111 -0
  30. data/spec/features/not_requirements_spec.rb +81 -0
  31. data/spec/features/state_and_array_options_accessor_spec.rb +47 -0
  32. data/spec/features/transition_boolean_comparison.rb +90 -0
  33. data/spec/helper.rb +33 -0
  34. data/spec/integration/active_record_persistence_spec.rb +0 -1
  35. data/spec/integration/example_01_document_spec.rb +1 -1
  36. data/spec/integration/relaxdb_persistence_spec.rb +94 -0
  37. data/spec/integration/requirement_reflection_spec.rb +2 -2
  38. data/spec/integration/transition_spec.rb +9 -1
  39. data/spec/units/binding_spec.rb +46 -17
  40. data/spec/units/lathe_spec.rb +11 -10
  41. data/spec/units/method_factory_spec.rb +6 -1
  42. metadata +37 -23
  43. 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
- 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
- require 'state_fu/mock_transition'
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
@@ -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.info( "Persister (#{@persister.class}) added: #{method_name} as field #{field_name}" )
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 events which can fire from the current_state
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
- # initialize a new transition
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
- # sanitize args for fire! and fireable?
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 "valid" (all requirements met)
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
- def evaluate_requirement_with_transition( name, t )
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
- # TODO SPECME HACKERY CRUFT FIXME THISSUCKS and needs *args
194
- # def evaluate_requirement_message( name, dest )
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) && current_state_name.is_a?(Symbol)
310
- other.to_sym == current_state_name || super( other )
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
@@ -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
- unless Object.const_defined?('ActiveSupport')
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
@@ -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
- def origins=( *args )
32
- if [args].flatten == [:ALL]
33
- @origins = machine.states
34
- else
35
- @origins = machine.find_or_create_states_by_name( *args.flatten ).extend( StateArray )
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
- if [args].flatten == [:ALL]
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
- # complete?(:origins) # do we have an origins?
48
- # complete? # do we have an origins and targets?
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
@@ -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
@@ -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 ]