davidlee-state-fu 0.3.1 → 0.10.0

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 (90) hide show
  1. data/README.textile +124 -34
  2. data/Rakefile +36 -30
  3. data/lib/no_stdout.rb +1 -1
  4. data/lib/state-fu.rb +9 -8
  5. data/lib/state_fu/active_support_lite/array/access.rb +12 -5
  6. data/lib/state_fu/active_support_lite/array/conversions.rb +10 -4
  7. data/lib/state_fu/active_support_lite/array/extract_options.rb +5 -4
  8. data/lib/state_fu/active_support_lite/array/grouping.rb +7 -4
  9. data/lib/state_fu/active_support_lite/array/random_access.rb +4 -3
  10. data/lib/state_fu/active_support_lite/array/wrapper.rb +4 -3
  11. data/lib/state_fu/active_support_lite/array.rb +3 -1
  12. data/lib/state_fu/active_support_lite/blank.rb +18 -9
  13. data/lib/state_fu/active_support_lite/cattr_reader.rb +4 -1
  14. data/lib/state_fu/active_support_lite/keys.rb +8 -3
  15. data/lib/state_fu/active_support_lite/misc.rb +6 -4
  16. data/lib/state_fu/active_support_lite/module/delegation.rb +130 -0
  17. data/lib/state_fu/active_support_lite/module.rb +1 -0
  18. data/lib/state_fu/active_support_lite/object.rb +5 -2
  19. data/lib/state_fu/active_support_lite/string.rb +6 -1
  20. data/lib/state_fu/active_support_lite/symbol.rb +2 -1
  21. data/lib/state_fu/applicable.rb +41 -0
  22. data/lib/state_fu/{helper.rb → arrays.rb} +45 -121
  23. data/lib/state_fu/binding.rb +136 -159
  24. data/lib/state_fu/core_ext.rb +78 -10
  25. data/lib/state_fu/event.rb +112 -48
  26. data/lib/state_fu/exceptions.rb +80 -34
  27. data/lib/state_fu/executioner.rb +149 -0
  28. data/lib/state_fu/has_options.rb +16 -0
  29. data/lib/state_fu/hooks.rb +21 -16
  30. data/lib/state_fu/interface.rb +80 -83
  31. data/lib/state_fu/lathe.rb +361 -148
  32. data/lib/state_fu/logger.rb +122 -45
  33. data/lib/state_fu/machine.rb +60 -32
  34. data/lib/state_fu/method_factory.rb +180 -72
  35. data/lib/state_fu/methodical.rb +17 -0
  36. data/lib/state_fu/persistence/active_record.rb +6 -1
  37. data/lib/state_fu/persistence/attribute.rb +1 -0
  38. data/lib/state_fu/persistence/base.rb +8 -6
  39. data/lib/state_fu/persistence.rb +94 -23
  40. data/lib/state_fu/sprocket.rb +26 -11
  41. data/lib/state_fu/state.rb +8 -27
  42. data/lib/state_fu/transition.rb +207 -98
  43. data/lib/state_fu/transition_query.rb +214 -0
  44. data/lib/state_fu.rb +1 -0
  45. data/lib/tasks/spec_last.rake +46 -0
  46. data/lib/tasks/state_fu.rake +57 -0
  47. data/lib/vizier.rb +61 -61
  48. data/spec/custom_formatter.rb +49 -0
  49. data/spec/features/binding_and_transition_helper_mixin_spec.rb +2 -2
  50. data/spec/features/method_missing_only_once_spec.rb +28 -0
  51. data/spec/features/not_requirements_spec.rb +83 -46
  52. data/spec/features/plotter_spec.rb +97 -0
  53. data/spec/features/shared_log_spec.rb +7 -0
  54. data/spec/features/singleton_machine_spec.rb +39 -0
  55. data/spec/features/state_and_array_options_accessor_spec.rb +1 -1
  56. data/spec/features/{transition_boolean_comparison.rb → transition_boolean_comparison_spec.rb} +29 -18
  57. data/spec/helper.rb +6 -117
  58. data/spec/integration/active_record_persistence_spec.rb +18 -4
  59. data/spec/integration/binding_extension_spec.rb +1 -1
  60. data/spec/integration/class_accessor_spec.rb +49 -59
  61. data/spec/integration/event_definition_spec.rb +20 -20
  62. data/spec/integration/example_01_document_spec.rb +13 -8
  63. data/spec/integration/example_02_string_spec.rb +3 -2
  64. data/spec/integration/instance_accessor_spec.rb +16 -19
  65. data/spec/integration/lathe_extension_spec.rb +2 -2
  66. data/spec/integration/machine_duplication_spec.rb +59 -37
  67. data/spec/integration/relaxdb_persistence_spec.rb +6 -3
  68. data/spec/integration/requirement_reflection_spec.rb +66 -57
  69. data/spec/integration/state_definition_spec.rb +72 -66
  70. data/spec/integration/transition_spec.rb +169 -173
  71. data/spec/spec.opts +5 -3
  72. data/spec/spec_helper.rb +132 -0
  73. data/spec/state_fu_spec.rb +870 -0
  74. data/spec/units/binding_spec.rb +33 -22
  75. data/spec/units/event_spec.rb +3 -22
  76. data/spec/units/exceptions_spec.rb +7 -0
  77. data/spec/units/lathe_spec.rb +7 -7
  78. data/spec/units/machine_spec.rb +67 -75
  79. data/spec/units/method_factory_spec.rb +55 -48
  80. data/spec/units/sprocket_spec.rb +5 -7
  81. data/spec/units/state_spec.rb +33 -24
  82. metadata +31 -19
  83. data/lib/state_fu/active_support_lite/inheritable_attributes.rb +0 -1
  84. data/lib/state_fu/fu_space.rb +0 -51
  85. data/lib/state_fu/mock_transition.rb +0 -38
  86. data/spec/BDD/plotter_spec.rb +0 -115
  87. data/spec/integration/dynamic_requirement_spec.rb +0 -160
  88. data/spec/integration/ex_machine_for_accounts_spec.rb +0 -79
  89. data/spec/integration/sanity_spec.rb +0 -31
  90. data/spec/units/fu_space_spec.rb +0 -95
@@ -24,11 +24,13 @@ module StateFu
24
24
  end
25
25
  end
26
26
 
27
+ # define this method in subclasses to do any preparation
27
28
  def self.prepare_field( klass, field_name )
28
- raise NotImplementedError # abstract method
29
+ Logger.warn("Abstract method in #{self}.prepare_field called. Override me!")
29
30
  end
30
31
 
31
32
  def initialize( binding, field_name )
33
+
32
34
  @binding = binding
33
35
  @field_name = field_name
34
36
  @current_state = find_current_state()
@@ -52,6 +54,10 @@ module StateFu
52
54
  end
53
55
  end
54
56
 
57
+ def reload
58
+ @current_state = find_current_state()
59
+ end
60
+
55
61
  def machine
56
62
  binding.machine
57
63
  end
@@ -61,13 +67,9 @@ module StateFu
61
67
  end
62
68
 
63
69
  def klass
64
- object.class
70
+ binding.target
65
71
  end
66
72
 
67
- # def method_name
68
- # binding.method_name
69
- # end
70
-
71
73
  def current_state=( state )
72
74
  raise(ArgumentError, state.inspect) unless state.is_a?(StateFu::State)
73
75
  @current_state = state
@@ -1,10 +1,100 @@
1
1
  module StateFu
2
+
3
+ # the persistence module has a few simple tests which help decide which
4
+ # persistence mechanism to use
5
+
6
+ # TODO add event hooks (on_change etc) ...
7
+ # after benchmarking
8
+
9
+ # To create your own custom persistence mechanism,
10
+ # subclass StateFu::Persistence::Base
11
+ # and define prepare_field, read_attribute and write_attribute:
12
+
13
+
14
+ # class StateFu::Persistence::MagneticCarpet < StateFu::Persistence::Base
15
+ # def prepare_field
16
+ #
17
+ #
18
+ # def read_attribute
19
+ # object.send "magnetised_#{field_name}"
20
+ # end
21
+ #
22
+ # def write_attribute( string_value )
23
+ # Logger.debug "magnetising ( #{field_name} => #{string_value} on #{object.inspect}"
24
+ # object.send "magnetised_#{field_name}=", string_value
25
+ # end
26
+ # end
27
+
2
28
  module Persistence
3
- DEFAULT_FIELD_NAME_SUFFIX = '_field'
29
+ DEFAULT_SUFFIX = '_field'
30
+ @@class_for = {}
31
+ @@fields_prepared = {}
32
+
33
+ #
34
+ # Class Methods
35
+ #
36
+
37
+ def self.default_field_name( machine_name )
38
+ machine_name == DEFAULT ? DEFAULT_FIELD : "#{machine_name.to_s.underscore.tr(' ','_')}#{DEFAULT_SUFFIX}"
39
+ end
40
+
41
+ # returns the appropriate persister class for the given class & field name.
42
+ def self.class_for( klass, field_name )
43
+ raise ArgumentError if [klass, field_name].any?(&:nil?)
44
+ @@class_for[klass] ||= {}
45
+ @@class_for[klass][field_name] ||=
46
+ if active_record_column?( klass, field_name )
47
+ self::ActiveRecord
48
+ elsif relaxdb_document_property?( klass, field_name )
49
+ self::RelaxDB
50
+ else
51
+ self::Attribute
52
+ end
53
+ end
54
+
55
+ def self.for_class( klass, binding, field_name )
56
+ persister_class = class_for klass, field_name
57
+ prepare_field( klass, field_name, persister_class)
58
+ returning persister_class.new( binding, field_name ) do |persister|
59
+ Logger.debug( "#{persister_class}: method #{binding.method_name} as field #{persister.field_name}" )
60
+ end
61
+ end
62
+
63
+ def self.for_instance( binding, field_name )
64
+ metaclass = class << binding.object; self; end
65
+ for_class( metaclass, binding, field_name )
66
+ end
67
+
68
+ # returns a new persister appropriate to the given binding and field_name
69
+ # also ensures the persister class method :prepare_field has been called
70
+ # once for the given class & field name so the field can be set up; eg an
71
+ # attr_accessor or a before_save hook defined
72
+ def self.for( binding )
73
+ field_name = binding.field_name.to_sym
74
+ if binding.singleton?
75
+ for_instance( binding, field_name )
76
+ else
77
+ for_class( binding.target, binding, field_name )
78
+ end
79
+ end
80
+
81
+ # ensures that <persister_class>.prepare_field is called only once
82
+ def self.prepare_field(klass, field_name, persister_class=nil)
83
+ @@fields_prepared[klass] ||= []
84
+ unless @@fields_prepared[klass].include?(field_name)
85
+ persister_class ||= class_for(klass, field_name)
86
+ persister_class.prepare_field( klass, field_name )
87
+ @@fields_prepared[klass] << field_name
88
+ end
89
+ end
90
+
91
+ #
92
+ # Heuristics - simple test methods to determine which persister to use
93
+ #
4
94
 
5
95
  # checks to see if the field_name for persistence is a
6
96
  # RelaxDB attribute.
7
- # Safe to use if RelaxDB is not included.
97
+ # Safe to use (skipped) if RelaxDB is not included.
8
98
  def self.relaxdb_document_property?( klass, field_name )
9
99
  Object.const_defined?('RelaxDB') &&
10
100
  klass.ancestors.include?( ::RelaxDB::Document ) &&
@@ -13,33 +103,14 @@ module StateFu
13
103
 
14
104
  # checks to see if the field_name for persistence is an
15
105
  # ActiveRecord column.
16
- # Safe to use if ActiveRecord is not included.
106
+ # Safe to use (skipped) if ActiveRecord is not included.
17
107
  def self.active_record_column?( klass, field_name )
18
108
  Object.const_defined?("ActiveRecord") &&
19
109
  ::ActiveRecord.const_defined?("Base") &&
20
110
  klass.ancestors.include?( ::ActiveRecord::Base ) &&
111
+ klass.table_exists? &&
21
112
  klass.columns.map(&:name).include?( field_name.to_s )
22
113
  end
23
114
 
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
30
- else
31
- self::Attribute
32
- end
33
- end
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
-
40
- def self.prepare_field( klass, field_name )
41
- class_for( klass, field_name ).prepare_field( klass, field_name )
42
- end
43
-
44
115
  end
45
116
  end
@@ -1,8 +1,11 @@
1
1
  module StateFu
2
- class Sprocket # Abstract Superclass of State & Event
3
- include StateFu::Helper # define apply!
4
-
5
- attr_reader :machine, :name, :options, :hooks
2
+ # the abstract superclass of State & Event
3
+ # defines behaviours shared by both classes
4
+ class Sprocket
5
+ include Applicable # define apply!
6
+ include HasOptions
7
+
8
+ attr_reader :machine, :name, :hooks
6
9
 
7
10
  def initialize(machine, name, options={})
8
11
  @machine = machine
@@ -18,6 +21,7 @@ module StateFu
18
21
  @hooks[slot.to_sym] << [name.to_sym, value]
19
22
  end
20
23
 
24
+ # yields a lathe for self; useful for updating machine definitions on the fly
21
25
  def lathe(options={}, &block)
22
26
  StateFu::Lathe.new( machine, self, options, &block )
23
27
  end
@@ -30,14 +34,25 @@ module StateFu
30
34
  "#<#{self.class}::#{self.object_id} @name=#{name.inspect}>"
31
35
  end
32
36
 
33
- def []v
34
- options[v]
35
- end
36
-
37
- def []=v,k
38
- options[v]=k
37
+ # allows state == <name> || event == <name> to return true
38
+ def == other
39
+ if other.is_a?(Symbol)
40
+ self.name == other
41
+ else
42
+ super other
43
+ end
44
+ end
45
+
46
+ # allows case equality tests against the state/event's name
47
+ # eg
48
+ # case state
49
+ # when :new
50
+ # ...
51
+ # end
52
+ def === other
53
+ self.to_sym === other.to_sym || super(other)
39
54
  end
40
-
55
+
41
56
  end
42
57
  end
43
58
 
@@ -1,11 +1,13 @@
1
1
  module StateFu
2
2
  class State < StateFu::Sprocket
3
3
 
4
- attr_reader :entry_requirements, :exit_requirements
5
-
4
+ attr_reader :entry_requirements, :exit_requirements, :own_events
5
+ alias_method :requirements, :entry_requirements
6
+
6
7
  def initialize(machine, name, options={})
7
8
  @entry_requirements = [].extend ArrayWithSymbolAccessor
8
9
  @exit_requirements = [].extend ArrayWithSymbolAccessor
10
+ @own_events = [].extend EventArray
9
11
  super( machine, name, options )
10
12
  end
11
13
 
@@ -13,33 +15,12 @@ module StateFu
13
15
  machine.events.from(self)
14
16
  end
15
17
 
16
- #
17
- # Proxy methods to StateFu::Lathe
18
- #
19
- # TODO - build something meta to build these proxy events
20
- def event( name, options={}, &block )
21
- if block_given?
22
- lathe.event( name, options, &block )
23
- else
24
- lathe.event( name, options )
25
- end
26
- end
27
-
28
- def enterable_by?( binding, *args )
29
- entry_requirements.reject do |r|
30
- res = binding.evaluate_requirement_with_args( r, *args )
31
- end.empty?
32
- end
33
-
34
- def exitable_by?( binding, *args )
35
- exit_requirements.reject do |r|
36
- binding.evaluate_requirement_with_args( r, *args )
37
- end.empty?
18
+ def before?(other)
19
+ machine.states.index(self) < machine.states.index(machine.states[other])
38
20
  end
39
21
 
40
- # allows @obj.state_fu.state === :new
41
- def === other
42
- self.to_sym === other.to_sym
22
+ def after?(other)
23
+ machine.states.index(self) > machine.states.index(machine.states[other])
43
24
  end
44
25
 
45
26
  # display nice and short
@@ -6,152 +6,208 @@ module StateFu
6
6
  # This is what gets yielded to event hooks; it also gets attached
7
7
  # to any TransitionHalted exceptions raised.
8
8
 
9
- class Transition
10
- include StateFu::Helper
11
- include ContextualEval
12
-
13
- attr_reader( :binding,
14
- :machine,
15
- :origin,
16
- :target,
17
- :event,
18
- :args,
19
- :errors,
20
- :object,
21
- :options,
22
- :current_hook_slot,
23
- :current_hook )
24
-
25
- attr_accessor :test_only, :args, :options
26
-
27
- def initialize( binding, event, target=nil, *args, &block )
28
- @binding = binding
29
- @machine = binding.machine
30
- @object = binding.object
31
- @origin = binding.current_state
9
+ # TODO - make transition evaluate as true if accepted, false if failed, or nil unless fired
32
10
 
11
+ class Transition
12
+ include Applicable
13
+ include HasOptions
14
+
15
+ attr_reader :binding,
16
+ :machine,
17
+ :origin,
18
+ :target,
19
+ :event,
20
+ :args,
21
+ :errors,
22
+ :object,
23
+ :current_hook_slot,
24
+ :current_hook
25
+
26
+ attr_accessor :test_only
27
+ alias_method :arguments, :args
28
+
29
+ def initialize( binding, event, target=nil, *argument_list, &block )
33
30
  # ensure event is a StateFu::Event
34
- if event.is_a?( Symbol ) && e = binding.machine.events[ event ]
31
+ if event.is_a?(Symbol) && e = binding.machine.events[event]
35
32
  event = e
36
33
  end
37
- raise( ArgumentError, "Not an event: #{event}" ) unless event.is_a?( StateFu::Event )
34
+ raise( ArgumentError, "Not an event: #{event}" ) unless event.is_a? Event
38
35
 
39
- target = find_event_target( event, target ) || raise( ArgumentError, "target cannot be determined: #{target.inspect}" )
40
-
41
- # ensure target is valid for the event
42
- unless event.targets.include?( target )
43
- raise( StateFu::InvalidTransition.new( binding, event, binding.current_state, target,
44
- "Illegal target #{target} for #{event}" ))
45
- end
46
-
47
- # ensure current_state is a valid origin for the event
48
- unless event.origins.include?( binding.current_state )
49
- raise( StateFu::InvalidTransition.new( binding, event, binding.current_state, target,
50
- "Illegal event #{event.name} for current state #{binding.state_name}" ))
51
- end
36
+ @binding = binding
37
+ @machine = binding.machine
38
+ @object = binding.object
39
+ @origin = binding.current_state
40
+
41
+ self.args= argument_list
42
+ apply!(argument_list, &block )
43
+
44
+ # ensure we have a target
45
+ target = find_event_target( event, target ) || raise( UnknownTarget.new(self, "target cannot be determined: #{target.inspect} #{self.inspect}"))
52
46
 
53
- @options = args.extract_options!.symbolize_keys!
54
47
  @target = target
55
48
  @event = event
56
- @args = args
57
49
  @errors = []
58
- @testing = @options.delete( :test_only )
50
+ @testing = @options.delete(:test_only)
51
+
52
+ if event.target_for_origin(origin) == target
53
+ # ...
54
+ else
55
+ # ensure target is valid for the event
56
+ unless event.targets.include? target
57
+ raise InvalidTransition.new self, "Illegal target #{target} for #{event}"
58
+ end
59
59
 
60
+ # ensure current_state is a valid origin for the event
61
+ unless event.origins.include? origin
62
+ raise InvalidTransition.new( self, "Illegal event #{event.name} for current state #{binding.state_name}" )
63
+ end
64
+ end
65
+
60
66
  machine.inject_helpers_into( self )
67
+ end
61
68
 
62
- # do stuff with the transition in a block, if you like
63
- apply!( &block ) if block_given?
69
+ def options=(opts={})
70
+ @options = opts
64
71
  end
65
72
 
73
+ def args=(a)
74
+ @args = a.extend(TransitionArgsArray).init(self)
75
+ apply!(a) if a.last.is_a?(Hash)
76
+ end
77
+
78
+ #
79
+ # Requirements
80
+ #
81
+
66
82
  def requirements
67
83
  origin.exit_requirements + target.entry_requirements + event.requirements
68
84
  end
69
85
 
70
- def unmet_requirements
71
- requirements.reject do |requirement|
72
- binding.evaluate_requirement_with_transition( requirement, self )
86
+ def unmet_requirements(revalidate=false, fail_fast=false) # TODO
87
+ if revalidate
88
+ return @unmet_requirements if @unmet_requirements
89
+ else
90
+ @unmet_requirements = nil
91
+ end
92
+ result = requirements.uniq.inject([]) do |unmet, requirement|
93
+ next if fail_fast && !unmet.empty?
94
+ unmet << requirement unless evaluate(requirement)
95
+ unmet
73
96
  end
97
+ @unmet_requirements = result if (!fail_fast || unmet_requirements.length <= 1)
98
+ result
99
+ end
100
+
101
+ def first_unmet_requirement(revalidate=false)
102
+ unmet_requirements(revalidate, fail_fast=true)[0]
74
103
  end
75
104
 
76
- def evaluate_requirement_message( name )
77
- msg = machine.requirement_messages[name]
78
- case msg
79
- when String, nil
80
- msg
81
- when Symbol, Proc
82
- evaluate_named_proc_or_method( msg, self )
83
- else
84
- raise msg.class.to_s
85
- end
105
+ def unmet_requirement_messages(revalidate=false, fail_fast=false) # TODO
106
+ unmet_requirements(revalidate, fail_fast).map do |requirement|
107
+ evaluate_requirement_message requirement
108
+ end.extend MessageArray
109
+ end
110
+ alias_method :error_messages, :unmet_requirement_messages
111
+
112
+ def requirement_errors(revalidate=false, fail_fast=false)
113
+ unmet_requirements(revalidate, fail_fast).
114
+ map { |requirement| [requirement, evaluate_requirement_message(requirement)]}.
115
+ to_h
86
116
  end
87
117
 
88
- def unmet_requirement_messages
89
- unmet_requirements.map do |requirement|
90
- evaluate_requirement_message( requirement )
91
- end
118
+ def first_unmet_requirement(revalidate=false)
119
+ unmet_requirements(revalidate, fail_fast=true)[0]
92
120
  end
93
121
 
94
- def check_requirements!
95
- raise RequirementError.new( self, unmet_requirements.inspect ) unless requirements_met?
122
+ def first_unmet_requirement_message(revalidate=false)
123
+ unmet_requirement_messages(revalidate, fail_fast=true)[0]
96
124
  end
97
125
 
98
- def requirements_met?
99
- unmet_requirements.empty?
126
+ def check_requirements!(revalidate=false, fail_fast=true) # TODO
127
+ raise RequirementError.new( self, unmet_requirement_messages.inspect ) unless requirements_met?(revalidate, fail_fast)
100
128
  end
101
- alias_method :valid?, :requirements_met?
102
129
 
103
- def hooks_for( element, slot )
130
+ def requirements_met?(revalidate=false, fail_fast=false) # TODO
131
+ unmet_requirements(revalidate, fail_fast).empty?
132
+ end
133
+ alias_method :valid?, :requirements_met?
134
+
135
+ #
136
+ # Hooks
137
+ #
138
+ def hooks_for(element, slot)
104
139
  send(element).hooks[slot]
105
140
  end
106
141
 
107
- def hooks()
142
+ def hooks
108
143
  StateFu::Hooks::ALL_HOOKS.map do |owner, slot|
109
- [ [owner, slot], send( owner ).hooks[ slot ] ]
144
+ [ [owner, slot], send(owner).hooks[slot] ]
110
145
  end
111
146
  end
112
147
 
113
- def current_state
114
- if accepted?
115
- :accepted
116
- else
117
- current_hook.state rescue :unfired
118
- end
148
+ def run_hook hook
149
+ evaluate hook
119
150
  end
120
151
 
121
- def run_hook( hook )
122
- evaluate_named_proc_or_method( hook, self )
123
- end
124
152
 
125
- def halt!( message )
153
+
154
+ #
155
+ #
156
+ #
157
+
158
+ # halt a transition with a message
159
+ # can be used to back out of a transition inside eg a state entry hook
160
+ def halt! message
126
161
  raise TransitionHalted.new( self, message )
127
162
  end
128
163
 
164
+ #
165
+ #
166
+ #
167
+
168
+ # actually fire the transition
129
169
  def fire!
130
- return false if fired? # no infinite loops please
170
+ raise TransitionAlreadyFired.new(self) if fired?
171
+ # return false if fired? # no infinite loops please
131
172
  check_requirements!
132
173
  @fired = true
133
174
  begin
175
+ # duplicated: see #hooks method
134
176
  StateFu::Hooks::ALL_HOOKS.map do |owner, slot|
135
- [ [owner, slot], send( owner ).hooks[ slot ] ]
177
+ [ [owner, slot], send(owner).hooks[slot] ]
136
178
  end.each do |address, hooks|
179
+ Logger.info("running #{address.inspect} hooks for #{object.class} #{object}")
137
180
  owner,slot = *address
138
181
  hooks.each do |hook|
182
+ Logger.info("running hook #{hooks} for #{object.class} #{object}")
139
183
  @current_hook_slot = address
140
184
  @current_hook = hook
141
- run_hook( hook )
185
+ run_hook hook
142
186
  end
143
187
  if slot == :entry
144
188
  @accepted = true
145
189
  @binding.persister.current_state = @target
190
+ Logger.info("State is now :#{@target.name} for #{object.class} #{object}")
146
191
  end
147
192
  end
148
193
  # transition complete
149
194
  @current_hook_slot = nil
150
195
  @current_hook = nil
151
196
  rescue TransitionHalted => e
197
+ Logger.info("Transition halted for #{object.class} #{object}: #{e.inspect}")
152
198
  @errors << e
153
199
  end
154
- return accepted?
200
+ self
201
+ end
202
+
203
+ #
204
+ # It can pretend it's a hash; so the transition makes a good argument to be
205
+ # passed to methods.
206
+ #
207
+ include Enumerable
208
+
209
+ def each *a, &b
210
+ options.each *a, &b
155
211
  end
156
212
 
157
213
  def halted?
@@ -173,9 +229,18 @@ module StateFu
173
229
  def accepted?
174
230
  !!@accepted
175
231
  end
176
-
232
+ alias_method :complete?, :accepted?
233
+
234
+ def current_state
235
+ binding.current_state
236
+ end
237
+
238
+ def destination
239
+ [event, target].map(&:to_sym)
240
+ end
241
+
177
242
  #
178
- # Try to give as many options (chances) as possible
243
+ # give as many choices as possible
179
244
  #
180
245
 
181
246
  alias_method :obj, :object
@@ -191,24 +256,10 @@ module StateFu
191
256
  alias_method :initial_state, :origin
192
257
  alias_method :from, :origin
193
258
 
194
- alias_method :om, :binding
195
- alias_method :stateful, :binding
196
- alias_method :binding, :binding
197
- alias_method :present, :binding
198
-
199
- alias_method :workflow, :machine
200
-
201
- alias_method :write? , :live?
202
- alias_method :destructive?, :live?
203
- alias_method :real?, :live?
204
- alias_method :really?, :live?
205
- alias_method :seriously?, :live?
206
-
207
259
  alias_method :test?, :testing?
208
260
  alias_method :test_only?, :testing?
209
261
  alias_method :read_only?, :testing?
210
262
  alias_method :only_pretend?, :testing?
211
- alias_method :pretend?, :testing?
212
263
  alias_method :dry_run?, :testing?
213
264
 
214
265
  # an accepted transition == true
@@ -220,9 +271,67 @@ module StateFu
220
271
  accepted?
221
272
  when false
222
273
  !accepted?
274
+ when State, Symbol
275
+ current_state == other.to_sym
276
+ when Transition
277
+ inspect == other.inspect
223
278
  else
224
279
  super( other )
225
280
  end
226
281
  end
282
+
283
+ # display nice and short
284
+ def inspect
285
+ s = self.to_s
286
+ s = s[0,s.length-1]
287
+ s << " event=#{event.to_sym.inspect}" if event
288
+ s << " origin=#{origin.to_sym.inspect}" if origin
289
+ s << " target=#{target.to_sym.inspect}" if target
290
+ s << " args=#{args.inspect}" if args
291
+ s << " options=#{options.inspect}" if options
292
+ s << ">"
293
+ s
294
+ end
295
+
296
+ private
297
+
298
+ def executioner
299
+ @executioner ||= Executioner.new( self ) do |ex|
300
+ machine.inject_helpers_into( ex )
301
+ machine.inject_methods_into( ex )
302
+ end
303
+ end
304
+
305
+ def evaluate(method_name_or_proc)
306
+ executioner.evaluate(method_name_or_proc)
307
+ end
308
+
309
+ def evaluate_requirement_message( name )
310
+ msg = machine.requirement_messages[name]
311
+ case msg
312
+ when String
313
+ msg
314
+ when nil
315
+ name
316
+ when Symbol, Proc
317
+ evaluate msg
318
+ else
319
+ raise msg.class.to_s
320
+ end
321
+ end
322
+
323
+ def find_event_target( evt, tgt )
324
+ case tgt
325
+ when StateFu::State
326
+ tgt
327
+ when Symbol
328
+ binding && binding.machine.states[ tgt ] # || raise( tgt.inspect )
329
+ when NilClass
330
+ evt.respond_to?(:target) && evt.target
331
+ else
332
+ raise ArgumentError.new( "#{tgt.class} is not a Symbol, StateFu::State or nil (#{evt})" )
333
+ end
334
+ end
335
+
227
336
  end
228
337
  end