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.
- data/README.textile +124 -34
- data/Rakefile +36 -30
- data/lib/no_stdout.rb +1 -1
- data/lib/state-fu.rb +9 -8
- data/lib/state_fu/active_support_lite/array/access.rb +12 -5
- data/lib/state_fu/active_support_lite/array/conversions.rb +10 -4
- data/lib/state_fu/active_support_lite/array/extract_options.rb +5 -4
- data/lib/state_fu/active_support_lite/array/grouping.rb +7 -4
- data/lib/state_fu/active_support_lite/array/random_access.rb +4 -3
- data/lib/state_fu/active_support_lite/array/wrapper.rb +4 -3
- data/lib/state_fu/active_support_lite/array.rb +3 -1
- data/lib/state_fu/active_support_lite/blank.rb +18 -9
- data/lib/state_fu/active_support_lite/cattr_reader.rb +4 -1
- data/lib/state_fu/active_support_lite/keys.rb +8 -3
- data/lib/state_fu/active_support_lite/misc.rb +6 -4
- data/lib/state_fu/active_support_lite/module/delegation.rb +130 -0
- data/lib/state_fu/active_support_lite/module.rb +1 -0
- data/lib/state_fu/active_support_lite/object.rb +5 -2
- data/lib/state_fu/active_support_lite/string.rb +6 -1
- data/lib/state_fu/active_support_lite/symbol.rb +2 -1
- data/lib/state_fu/applicable.rb +41 -0
- data/lib/state_fu/{helper.rb → arrays.rb} +45 -121
- data/lib/state_fu/binding.rb +136 -159
- data/lib/state_fu/core_ext.rb +78 -10
- data/lib/state_fu/event.rb +112 -48
- data/lib/state_fu/exceptions.rb +80 -34
- data/lib/state_fu/executioner.rb +149 -0
- data/lib/state_fu/has_options.rb +16 -0
- data/lib/state_fu/hooks.rb +21 -16
- data/lib/state_fu/interface.rb +80 -83
- data/lib/state_fu/lathe.rb +361 -148
- data/lib/state_fu/logger.rb +122 -45
- data/lib/state_fu/machine.rb +60 -32
- data/lib/state_fu/method_factory.rb +180 -72
- data/lib/state_fu/methodical.rb +17 -0
- data/lib/state_fu/persistence/active_record.rb +6 -1
- data/lib/state_fu/persistence/attribute.rb +1 -0
- data/lib/state_fu/persistence/base.rb +8 -6
- data/lib/state_fu/persistence.rb +94 -23
- data/lib/state_fu/sprocket.rb +26 -11
- data/lib/state_fu/state.rb +8 -27
- data/lib/state_fu/transition.rb +207 -98
- data/lib/state_fu/transition_query.rb +214 -0
- data/lib/state_fu.rb +1 -0
- data/lib/tasks/spec_last.rake +46 -0
- data/lib/tasks/state_fu.rake +57 -0
- data/lib/vizier.rb +61 -61
- data/spec/custom_formatter.rb +49 -0
- data/spec/features/binding_and_transition_helper_mixin_spec.rb +2 -2
- data/spec/features/method_missing_only_once_spec.rb +28 -0
- data/spec/features/not_requirements_spec.rb +83 -46
- data/spec/features/plotter_spec.rb +97 -0
- data/spec/features/shared_log_spec.rb +7 -0
- data/spec/features/singleton_machine_spec.rb +39 -0
- data/spec/features/state_and_array_options_accessor_spec.rb +1 -1
- data/spec/features/{transition_boolean_comparison.rb → transition_boolean_comparison_spec.rb} +29 -18
- data/spec/helper.rb +6 -117
- data/spec/integration/active_record_persistence_spec.rb +18 -4
- data/spec/integration/binding_extension_spec.rb +1 -1
- data/spec/integration/class_accessor_spec.rb +49 -59
- data/spec/integration/event_definition_spec.rb +20 -20
- data/spec/integration/example_01_document_spec.rb +13 -8
- data/spec/integration/example_02_string_spec.rb +3 -2
- data/spec/integration/instance_accessor_spec.rb +16 -19
- data/spec/integration/lathe_extension_spec.rb +2 -2
- data/spec/integration/machine_duplication_spec.rb +59 -37
- data/spec/integration/relaxdb_persistence_spec.rb +6 -3
- data/spec/integration/requirement_reflection_spec.rb +66 -57
- data/spec/integration/state_definition_spec.rb +72 -66
- data/spec/integration/transition_spec.rb +169 -173
- data/spec/spec.opts +5 -3
- data/spec/spec_helper.rb +132 -0
- data/spec/state_fu_spec.rb +870 -0
- data/spec/units/binding_spec.rb +33 -22
- data/spec/units/event_spec.rb +3 -22
- data/spec/units/exceptions_spec.rb +7 -0
- data/spec/units/lathe_spec.rb +7 -7
- data/spec/units/machine_spec.rb +67 -75
- data/spec/units/method_factory_spec.rb +55 -48
- data/spec/units/sprocket_spec.rb +5 -7
- data/spec/units/state_spec.rb +33 -24
- metadata +31 -19
- data/lib/state_fu/active_support_lite/inheritable_attributes.rb +0 -1
- data/lib/state_fu/fu_space.rb +0 -51
- data/lib/state_fu/mock_transition.rb +0 -38
- data/spec/BDD/plotter_spec.rb +0 -115
- data/spec/integration/dynamic_requirement_spec.rb +0 -160
- data/spec/integration/ex_machine_for_accounts_spec.rb +0 -79
- data/spec/integration/sanity_spec.rb +0 -31
- 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
|
-
|
|
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
|
-
|
|
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
|
data/lib/state_fu/persistence.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/state_fu/sprocket.rb
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
module StateFu
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
data/lib/state_fu/state.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
data/lib/state_fu/transition.rb
CHANGED
|
@@ -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
|
-
|
|
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?(
|
|
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?
|
|
34
|
+
raise( ArgumentError, "Not an event: #{event}" ) unless event.is_a? Event
|
|
38
35
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
# ensure
|
|
48
|
-
|
|
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(
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
89
|
-
unmet_requirements
|
|
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
|
|
95
|
-
|
|
122
|
+
def first_unmet_requirement_message(revalidate=false)
|
|
123
|
+
unmet_requirement_messages(revalidate, fail_fast=true)[0]
|
|
96
124
|
end
|
|
97
125
|
|
|
98
|
-
def
|
|
99
|
-
|
|
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
|
|
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(
|
|
144
|
+
[ [owner, slot], send(owner).hooks[slot] ]
|
|
110
145
|
end
|
|
111
146
|
end
|
|
112
147
|
|
|
113
|
-
def
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|