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
@@ -79,10 +79,12 @@ module StateFu
79
79
  # is there exactly one possible event to fire, with a single
80
80
  # target event?
81
81
  def next?
82
+ raise NotImplementedError
82
83
  end
83
84
 
84
85
  # if next?, return the state
85
86
  def next
87
+ raise NotImplementedError
86
88
  end
87
89
 
88
90
  end
@@ -104,10 +106,12 @@ module StateFu
104
106
  # is there exactly one possible event to fire, with a single
105
107
  # target event?
106
108
  def next?
109
+ raise NotImplementedError
107
110
  end
108
111
 
109
112
  # if next?, return the event
110
113
  def next
114
+ raise NotImplementedError
111
115
  end
112
116
 
113
117
  end
@@ -117,7 +121,9 @@ module StateFu
117
121
  self.map do |h|
118
122
  case h
119
123
  when String, Symbol
120
- Object.const_get( h.to_s.classify )
124
+ mod_name = h.to_s.split('/').inject(Object) do |mod, part|
125
+ mod = mod.const_get( part.camelize )
126
+ end
121
127
  when Module
122
128
  h
123
129
  else
@@ -182,11 +188,14 @@ module StateFu
182
188
  end
183
189
  end # OrderedHash
184
190
 
191
+ # satanic incantations we use for evaluating blocks conditionally,
192
+ # massaging their arguments and managing execution context.
185
193
  module ContextualEval
194
+ # :nodoc:
186
195
  module InstanceMethods
187
196
 
188
197
  # if we use &block syntax it stuffs the arity up, so we have to
189
- # pass it as a normal argument
198
+ # pass it as a normal argument. Ruby bug!
190
199
  def limit_arguments( block, *args )
191
200
  case block.arity
192
201
  when -1, 0
@@ -225,8 +234,21 @@ module StateFu
225
234
  def evaluate_named_proc_or_method( name, *args )
226
235
  if (name.is_a?( Proc ) && proc = name) || proc = machine.named_procs[ name ]
227
236
  evaluate( *args, &proc )
228
- else
237
+ elsif self.respond_to?( name )
238
+ if method(name).arity == 0
239
+ send(name)
240
+ else
241
+ send(name, *args )
242
+ end
243
+ # evaluate( *args, &method(name) )
244
+ elsif object.respond_to?( name )
229
245
  call_on_object_with_optional_args( name, *args )
246
+ else # method is not defined
247
+ if name.to_s =~ /^not_(.*)$/
248
+ !evaluate_named_proc_or_method( $1, *args )
249
+ else
250
+ raise NoMethodError.new("#{name} is not defined on #{object} or #{self} or as a named proc in #{machine}")
251
+ end
230
252
  end
231
253
  end
232
254
 
@@ -1,5 +1,8 @@
1
1
  module StateFu
2
- module Hooks
2
+
3
+ # TODO document structure / sequence of hooks elsewhere
4
+
5
+ module Hooks # :nodoc:
3
6
 
4
7
  ALL_HOOKS = [[:event, :before], # good place to start a transaction, etc
5
8
  [:origin, :exit], # say goodbye!
@@ -6,9 +6,8 @@ module StateFu
6
6
 
7
7
  # TODO:
8
8
  # take option :alias => false (disable aliases) or :alias
9
- # => :foo (use foo as class & instance accessor)
9
+ # => :foo (add :foo as class & instance accessor methods)
10
10
 
11
- #
12
11
  # Given no arguments, return the default machine (:state_fu) for the
13
12
  # class, creating it if it did not exist.
14
13
  #
@@ -35,10 +34,9 @@ module StateFu
35
34
  alias_method :stfu, :machine
36
35
  alias_method :state_fu, :machine
37
36
  alias_method :workflow, :machine
37
+ alias_method :stateful, :machine
38
38
  alias_method :statefully, :machine
39
39
  alias_method :state_machine, :machine
40
- alias_method :stateful, :machine
41
- alias_method :workflow, :machine
42
40
  alias_method :engine, :machine
43
41
 
44
42
  # return a hash of :name => StateFu::Machine for your class.
@@ -49,22 +47,25 @@ module StateFu
49
47
  machine( *args, &block)
50
48
  end
51
49
  end
52
- alias_method :machines, :machines
53
- alias_method :workflows, :machines
54
- alias_method :engines, :machines
50
+ alias_method :stfus, :machines
51
+ alias_method :state_fus, :machines
52
+ alias_method :workflows, :machines
53
+ alias_method :engines, :machines
55
54
 
56
55
  # return the list of machines names for this class
57
56
  def machine_names()
58
57
  StateFu::FuSpace.class_machines[self].keys
59
58
  end
60
- alias_method :machine_names, :machine_names
61
- alias_method :workflow_names, :machine_names
62
- alias_method :engine_names, :machine_names
59
+ alias_method :stfu_names, :machine_names
60
+ alias_method :state_fu_names, :machine_names
61
+ alias_method :workflow_names, :machine_names
62
+ alias_method :engine_names, :machine_names
63
63
  end
64
64
 
65
- # Give the gift of state to your objects. These methods
66
- # grant access to StateFu::Binding objects, which are bundles of
67
- # context linking a StateFu::Machine to an object / instance.
65
+ # These methods grant access to StateFu::Binding objects, which
66
+ # are bundles of context encapsulating a StateFu::Machine, an instance
67
+ # of a class, and its current state in the machine.
68
+
68
69
  # Again, plenty of aliases are provided so you can use whatever
69
70
  # makes sense to you.
70
71
  module InstanceMethods
@@ -73,16 +74,11 @@ module StateFu
73
74
  @_state_fu ||= {}
74
75
  end
75
76
 
76
- # A StateFu::Binding comes into being, linking your object and a
77
- # machine, when you first call yourobject.binding() for that
78
- # machine.
79
- #
80
- # Like the class method .machine(), calling it without any arguments
81
- # is equivalent to passing :om.
82
- #
83
- # Essentially, this is the accessor method through which an instance
84
- # can see and change its state, interact with events, etc.
77
+ # A StateFu::Binding comes into being when it is first referenced.
85
78
  #
79
+ # This is the accessor method through which an object instance (or developer)
80
+ # can access a StateFu::Machine, the object's current state, the
81
+ # methods which trigger event transitions, etc.
86
82
  public
87
83
  def _binding( name=StateFu::DEFAULT_MACHINE )
88
84
  name = name.to_sym
@@ -97,9 +93,9 @@ module StateFu
97
93
  alias_method :stateful, :_binding
98
94
  alias_method :workflow, :_binding
99
95
  alias_method :engine, :_binding
96
+ alias_method :machine, :_binding # not strictly accurate
100
97
  alias_method :context, :_binding
101
98
 
102
-
103
99
  # Gain awareness of all bindings (state contexts) this object
104
100
  # has contemplated into being.
105
101
  # Returns a Hash of { :name => <StateFu::Binding>, ... }
@@ -114,7 +110,7 @@ module StateFu
114
110
  alias_method :workflows, :_bindings
115
111
  alias_method :engines, :_bindings
116
112
  alias_method :bindings, :_bindings
117
- alias_method :machines, :_bindings # not strictly accurate, but makes sense sometimes
113
+ alias_method :machines, :_bindings # not strictly accurate
118
114
  alias_method :contexts, :_bindings
119
115
 
120
116
  # Instantiate bindings for all machines defined for this class.
@@ -1,10 +1,17 @@
1
1
  module StateFu
2
+ # A Lathe parses and a Machine definition and returns a freshly turned
3
+ # Machine.
4
+ #
5
+ # It provides the means to define the arrangement of StateFu objects
6
+ # ( eg States and Events) which comprise a workflow, process,
7
+ # lifecycle, circuit, syntax, etc.
2
8
  class Lathe
3
9
 
4
10
  # NOTE: Sprocket is the abstract superclass of Event and State
5
11
 
6
12
  attr_reader :machine, :sprocket, :options
7
13
 
14
+ # you don't need to call this directly.
8
15
  def initialize( machine, sprocket = nil, options={}, &block )
9
16
  @machine = machine
10
17
  @sprocket = sprocket
@@ -141,7 +148,7 @@ module StateFu
141
148
  if child? && sprocket.is_a?( StateFu::State ) # in state block
142
149
  targets = options.delete(:to)
143
150
  evt = define_event( name, options, &block )
144
- evt.from sprocket unless evt.origins
151
+ evt.from sprocket unless sprocket.nil?
145
152
  evt.to( targets ) unless targets.nil?
146
153
  evt
147
154
  else # in master lathe
@@ -225,11 +232,38 @@ module StateFu
225
232
  sprocket.to( *args, &block )
226
233
  end
227
234
 
235
+ #
236
+ # define chained events and states succinctly
237
+ # usage: chain 'state1 -event1-> state2 -event2-> state3'
238
+ def chain (string)
239
+ rx_word = /([a-zA-Z0-9_]+)/
240
+ rx_state = /^#{rx_word}$/
241
+ rx_event = /^-#{rx_word}->$/
242
+ previous = nil
243
+ string.split.each do |chunk|
244
+ case chunk
245
+ when rx_state
246
+ current = state($1)
247
+ if previous.is_a?( StateFu::Event )
248
+ previous.to( current )
249
+ end
250
+ when rx_event
251
+ current = event($1)
252
+ if previous.is_a?( StateFu::State )
253
+ current.from( previous )
254
+ end
255
+ else
256
+ raise ArgumentError, "'#{chunk}' is not a valid token"
257
+ end
258
+ previous = current
259
+ end
260
+ end
261
+
228
262
  #
229
263
  # do something with all states / events
230
264
  #
231
265
  def each_sprocket( type, *args, &block)
232
- require_no_sprocket()
266
+
233
267
  options = args.extract_options!.symbolize_keys!
234
268
  if args == [:ALL] || args == []
235
269
  args = machine.send("#{type}s").except( options.delete(:except) )
@@ -238,12 +272,14 @@ module StateFu
238
272
  end
239
273
 
240
274
  def states( *args, &block )
275
+ require_no_sprocket()
241
276
  each_sprocket( 'state', *args, &block )
242
277
  end
243
278
  alias_method :all_states, :states
244
279
  alias_method :each_state, :states
245
280
 
246
281
  def events( *args, &block )
282
+ require_sprocket( NilClass, StateFu::State )
247
283
  each_sprocket( 'event', *args, &block )
248
284
  end
249
285
  alias_method :all_events, :events
@@ -1,10 +1,88 @@
1
1
  require 'logger'
2
-
3
2
  module StateFu
4
- if Object.const_defined?( "RAILS_DEFAULT_LOGGER" )
5
- Logger = RAILS_DEFAULT_LOGGER
6
- else
7
- Logger = ::Logger.new( STDOUT )
8
- Logger.level = ::Logger.const_get( (ENV["ZEN_LOGLEVEL"] || 'WARN').upcase )
3
+ class Logger
4
+ cattr_accessor :prefix # prefix for log messages
5
+ cattr_accessor :suppress # set true to send messages to /dev/null
6
+
7
+ DEBUG = 0
8
+ INFO = 1
9
+ WARN = 2
10
+ ERROR = 3
11
+ FATAL = 4
12
+ UNKNOWN = 5
13
+
14
+ ENV_LOG_LEVEL = 'STATEFU_LOGLEVEL'
15
+ DEFAULT_LEVEL = INFO
16
+
17
+ DEFAULT_PREFIX = nil
18
+ SHARED_LOG_PREFIX = 'StateFu: '
19
+
20
+ @@prefix = DEFAULT_PREFIX
21
+ @@logger = nil
22
+ @@suppress = false
23
+
24
+ def self.level=( new_level )
25
+ instance.level = case new_level
26
+ when String, Symbol
27
+ const_get( new_level )
28
+ when Fixnum
29
+ new_level
30
+ else
31
+ state_fu_log_level()
32
+ end
33
+ end
34
+
35
+ def self.state_fu_log_level
36
+ if ENV[ ENV_LOG_LEVEL ]
37
+ const_get( ENV[ ENV_LOG_LEVEL ] )
38
+ else
39
+ DEFAULT_LEVEL
40
+ end
41
+ end
42
+
43
+ def self.new( log = $stdout, level = () )
44
+ self.instance = get_logger( log )
45
+ end
46
+
47
+ def self.instance=( logger )
48
+ @@logger ||= get_logger
49
+ end
50
+
51
+ def self.instance
52
+ @@logger ||= get_logger
53
+ end
54
+
55
+ def self.get_logger( log = $stdout )
56
+ if Object.const_defined?( "RAILS_DEFAULT_LOGGER" )
57
+ logger = RAILS_DEFAULT_LOGGER
58
+ prefix = SHARED_LOG_PREFIX
59
+ else
60
+ if Object.const_defined?( 'ActiveSupport' ) && ActiveSupport.const_defined?('BufferedLogger')
61
+ logger = ActiveSupport::BufferedLogger.new( log )
62
+ else
63
+ logger = ::Logger.new( log )
64
+ logger.level = state_fu_log_level()
65
+ end
66
+ end
67
+ logger
68
+ end
69
+
70
+ def self.suppress!
71
+ @@suppress = true
72
+ end
73
+
74
+ # method_missing is usually a last resort
75
+ # but i don't see it causing any headaches here.
76
+ def self.method_missing( method_id, *args )
77
+ return if @@suppress
78
+ if [:debug, :info, :warn, :error, :fatal].include?( method_id ) &&
79
+ args[0].is_a?(String) && @@prefix
80
+ args[0] = @@prefix + args[0]
81
+ end
82
+ instance.send( method_id, *args )
83
+ end
84
+
9
85
  end
10
86
  end
87
+
88
+ # StateFu::Logger.info( StateFu::Logger.instance.inspect )
@@ -149,5 +149,8 @@ module StateFu
149
149
  "#<#{self.class} ##{__id__} states=#{state_names.inspect} events=#{event_names.inspect} options=#{options.inspect}>"
150
150
  end
151
151
 
152
+ def graphviz
153
+ @graphviz ||= Plotter.new(self).output
154
+ end
152
155
  end
153
156
  end
@@ -78,14 +78,14 @@ module StateFu
78
78
  # obj.event_name?( target )
79
79
  # true if the event is fireable? (ie, requirements met)
80
80
  method_name = "#{event.name}?"
81
- define_method_on_metaclass( obj, method_name ) do |target|
82
- _binding.fireable?( [event, target] )
81
+ define_method_on_metaclass( obj, method_name ) do |target, *args|
82
+ _binding.fireable?( [event, target], *args )
83
83
  end
84
84
 
85
85
  # obj.event_name!( target, *args )
86
86
  # creates, fires and returns a transition
87
87
  method_name = "#{event.name}!"
88
- define_method_on_metaclass( obj, method_name ) do |target,*args|
88
+ define_method_on_metaclass( obj, method_name ) do |target, *args|
89
89
  _binding.fire!( [event, target], *args )
90
90
  end
91
91
 
@@ -4,6 +4,7 @@ module StateFu
4
4
 
5
5
  def self.prepare_field( klass, field_name )
6
6
  _field_name = field_name
7
+ Logger.debug("Preparing ActiveRecord field #{klass}.#{field_name}")
7
8
  klass.send :before_save, :state_fu!
8
9
  # validates_presence_of _field_name
9
10
  end
@@ -14,11 +15,12 @@ module StateFu
14
15
  # Attribute version, so just do the simplest thing we can.
15
16
 
16
17
  def read_attribute
18
+ Logger.debug "Read attribute #{field_name}, got #{object.send(:read_attribute,field_name)} for #{object.inspect}"
17
19
  object.send( :read_attribute, field_name )
18
20
  end
19
21
 
20
22
  def write_attribute( string_value )
21
- # Logger.warn(" :write_attribute,#{ field_name},#{ string_value} \n=========================================================")
23
+ Logger.debug "Write attribute #{field_name} to #{string_value} for #{object.inspect}"
22
24
  object.send( :write_attribute, field_name, string_value )
23
25
  end
24
26
 
@@ -5,7 +5,7 @@ module StateFu
5
5
  def self.prepare_field( klass, field_name )
6
6
  # ensure getter exists
7
7
  unless klass.instance_methods.map(&:to_sym).include?( field_name.to_sym )
8
- Logger.info "Adding attr_reader :#{field_name} for #{klass}"
8
+ Logger.debug "Adding attr_reader :#{field_name} for #{klass}"
9
9
  _field_name = field_name
10
10
  klass.class_eval do
11
11
  private
@@ -15,7 +15,7 @@ module StateFu
15
15
 
16
16
  # ensure setter exists
17
17
  unless klass.instance_methods.map(&:to_sym).include?( :"#{field_name}=" )
18
- Logger.info "Adding attr_writer :#{field_name}= for #{klass}"
18
+ Logger.debug "Adding attr_writer :#{field_name}= for #{klass}"
19
19
  _field_name = field_name
20
20
  klass.class_eval do
21
21
  private
@@ -31,13 +31,13 @@ module StateFu
31
31
 
32
32
  def read_attribute
33
33
  string = object.send( field_name )
34
- Logger.info "Read attribute #{field_name}, got #{string.inspect} for #{object.inspect}"
34
+ Logger.debug "Read attribute #{field_name}, got #{string.inspect} for #{object.inspect}"
35
35
  string
36
36
  end
37
37
 
38
38
  def write_attribute( string_value )
39
39
  writer_method = "#{field_name}="
40
- Logger.info "Writing attribute #{field_name} -> #{string_value.inspect} for #{object.inspect}"
40
+ Logger.debug "Writing attribute #{field_name} -> #{string_value.inspect} for #{object.inspect}"
41
41
  object.send( writer_method, string_value )
42
42
  end
43
43
 
@@ -34,11 +34,11 @@ module StateFu
34
34
  @current_state = find_current_state()
35
35
 
36
36
  if current_state.nil?
37
- Logger.info("Object has an undefined state: #{object}")
38
- Logger.info("Machine has no states: #{machine}") if machine.states.empty?
37
+ Logger.warn("undefined state for binding #{binding} on #{object} with field_name #{field_name.inspect}")
38
+ Logger.warn("Machine for #{object} has no states: #{machine}") if machine.states.empty?
39
39
  else
40
40
  persist!
41
- # Logger.debug("Object #{object} resuming #{binding.method_name} at #{current_state.name}: #{object.inspect}")
41
+ Logger.debug("#{object} resumes #{binding.method_name} at #{current_state.name}")
42
42
  end
43
43
  end
44
44
 
@@ -0,0 +1,23 @@
1
+ module StateFu
2
+ module Persistence
3
+ class RelaxDB < StateFu::Persistence::Base
4
+
5
+ def self.prepare_field( klass, field_name )
6
+ _field_name = field_name
7
+ #puts "relaxdb.before_save?"
8
+ end
9
+
10
+ private
11
+
12
+ def read_attribute
13
+ object.send( field_name )
14
+ end
15
+
16
+ def write_attribute( string_value )
17
+ object.send( "#{field_name}=", string_value )
18
+ end
19
+
20
+ end
21
+ end
22
+ end
23
+
@@ -2,27 +2,18 @@ module StateFu
2
2
  module Persistence
3
3
  DEFAULT_FIELD_NAME_SUFFIX = '_field'
4
4
 
5
- def self.prepare_class( klass )
6
- return if ( klass.instance_methods + klass.private_methods + klass.protected_methods ).map(&:to_sym).include?( :method_missing_before_state_fu )
7
- alias_method :method_missing_before_state_fu, :method_missing
8
- klass.class_eval do
9
- def method_missing( method_name, *args, &block )
10
- args.unshift method_name
11
- if @state_fu_initialized
12
- method_missing_before_state_fu( *args, &block )
13
- else
14
- state_fu!
15
- if respond_to?(method_name)
16
- send( *args, &block )
17
- else
18
- method_missing_before_state_fu( *args, &block )
19
- end
20
- end
21
- end # method_missing
22
- end # class_eval
23
- end # prepare_class
24
-
5
+ # checks to see if the field_name for persistence is a
6
+ # RelaxDB attribute.
7
+ # Safe to use if RelaxDB is not included.
8
+ def self.relaxdb_document_property?( klass, field_name )
9
+ Object.const_defined?('RelaxDB') &&
10
+ klass.ancestors.include?( ::RelaxDB::Document ) &&
11
+ klass.properties.map(&:to_s).include?( field_name.to_s )
12
+ end
25
13
 
14
+ # checks to see if the field_name for persistence is an
15
+ # ActiveRecord column.
16
+ # Safe to use if ActiveRecord is not included.
26
17
  def self.active_record_column?( klass, field_name )
27
18
  Object.const_defined?("ActiveRecord") &&
28
19
  ::ActiveRecord.const_defined?("Base") &&
@@ -30,20 +21,24 @@ module StateFu
30
21
  klass.columns.map(&:name).include?( field_name.to_s )
31
22
  end
32
23
 
33
- def self.for( binding, field_name )
34
- if active_record_column?( binding.object.class, field_name )
35
- self::ActiveRecord.new( binding, field_name )
24
+ # returns the appropriate persister class for the given class & field name.
25
+ def self.class_for( klass, field_name )
26
+ if active_record_column?( klass, field_name )
27
+ self::ActiveRecord
28
+ elsif relaxdb_document_property?( klass, field_name )
29
+ self::RelaxDB
36
30
  else
37
- self::Attribute.new( binding, field_name )
31
+ self::Attribute
38
32
  end
39
33
  end
40
34
 
35
+ # returns a persister appropriate to the given binding and field_name
36
+ def self.for( binding, field_name )
37
+ class_for( binding.object.class, field_name ).new( binding, field_name )
38
+ end
39
+
41
40
  def self.prepare_field( klass, field_name )
42
- if active_record_column?( klass, field_name )
43
- self::ActiveRecord.prepare_field( klass, field_name )
44
- else
45
- self::Attribute.prepare_field( klass, field_name )
46
- end
41
+ class_for( klass, field_name ).prepare_field( klass, field_name )
47
42
  end
48
43
 
49
44
  end
@@ -0,0 +1,63 @@
1
+ require File.join(File.dirname(__FILE__),'../vizier')
2
+ require 'tempfile'
3
+
4
+ module StateFu
5
+ class Plotter
6
+ attr_reader :machine, :dot, :graph, :states, :events
7
+
8
+ OUTPUT_HELPER = Module.new do
9
+
10
+ def save!
11
+ Tempfile.new(['state_fu_graph','.dot']) do |fh|
12
+ fh.write( self )
13
+ end.path
14
+ end
15
+
16
+ def save_as( filename )
17
+ File.open(filename, 'w') { |fh| fh.write( self ) }
18
+ end
19
+
20
+ def save_png(filename)
21
+ raise NotImplementedError
22
+ # dot graph.dot -Tpng -O
23
+ end
24
+
25
+ end
26
+
27
+ def output
28
+ generate
29
+ end
30
+
31
+ def initialize( machine, options={} )
32
+ raise RuntimeError, machine.class.to_s unless machine.is_a?(StateFu::Machine)
33
+ @machine = machine
34
+ @options = options.symbolize_keys!
35
+ @states = {}
36
+ @events = {}
37
+ # generate
38
+ end
39
+
40
+ def generate
41
+ @dot ||= generate_dot!.extend( OUTPUT_HELPER )
42
+ end
43
+
44
+ def generate_dot!
45
+ @graph = Vizier::Graph.new(:state_machine) do |g|
46
+ g.node :shape => 'doublecircle'
47
+ machine.state_names.map.each do |s|
48
+ @states[s] = g.add_node(s.to_s)
49
+ end
50
+ machine.events.map.each do |e|
51
+ e.origins.map(&:name).each do |from|
52
+ e.targets.map(&:name).each do |to|
53
+ g.connect( @states[from], @states[to], :label => e.name.to_s )
54
+ end
55
+ end
56
+ # @events[s] = g.add_node(s.to_s)
57
+ end
58
+ end
59
+ @graph.generate!
60
+ end
61
+
62
+ end
63
+ end
@@ -26,6 +26,18 @@ module StateFu
26
26
  raise NotImeplementedError # abstract
27
27
  end
28
28
 
29
+ def to_s
30
+ "#<#{self.class}::#{self.object_id} @name=#{name.inspect}>"
31
+ end
32
+
33
+ def []v
34
+ options[v]
35
+ end
36
+
37
+ def []=v,k
38
+ options[v]=k
39
+ end
40
+
29
41
  end
30
42
  end
31
43
 
@@ -41,5 +41,27 @@ module StateFu
41
41
  def === other
42
42
  self.to_sym === other.to_sym
43
43
  end
44
+
45
+ # display nice and short
46
+ def inspect
47
+ s = self.to_s
48
+ s = s[0,s.length-1]
49
+ display_hooks = hooks.dup
50
+ display_hooks.each do |k,v|
51
+ display_hooks.delete(k) if v.empty?
52
+ end
53
+ unless display_hooks.empty?
54
+ s << " hooks=#{display_hooks.inspect}"
55
+ end
56
+ unless entry_requirements.empty?
57
+ s << " entry_requirements=#{entry_requirements.inspect}"
58
+ end
59
+ unless exit_requirements.empty?
60
+ s << " exit_requirements=#{exit_requirements.inspect}"
61
+ end
62
+ s << ">"
63
+ s
64
+ end
65
+
44
66
  end
45
67
  end