state-fu 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +40 -0
- data/README.textile +293 -0
- data/Rakefile +114 -0
- data/lib/binding.rb +292 -0
- data/lib/event.rb +192 -0
- data/lib/executioner.rb +120 -0
- data/lib/hooks.rb +39 -0
- data/lib/interface.rb +132 -0
- data/lib/lathe.rb +538 -0
- data/lib/machine.rb +184 -0
- data/lib/method_factory.rb +243 -0
- data/lib/persistence.rb +116 -0
- data/lib/persistence/active_record.rb +34 -0
- data/lib/persistence/attribute.rb +47 -0
- data/lib/persistence/base.rb +100 -0
- data/lib/persistence/relaxdb.rb +23 -0
- data/lib/persistence/session.rb +7 -0
- data/lib/sprocket.rb +58 -0
- data/lib/state-fu.rb +56 -0
- data/lib/state.rb +48 -0
- data/lib/support/active_support_lite/array.rb +9 -0
- data/lib/support/active_support_lite/array/access.rb +60 -0
- data/lib/support/active_support_lite/array/conversions.rb +202 -0
- data/lib/support/active_support_lite/array/extract_options.rb +21 -0
- data/lib/support/active_support_lite/array/grouping.rb +109 -0
- data/lib/support/active_support_lite/array/random_access.rb +13 -0
- data/lib/support/active_support_lite/array/wrapper.rb +25 -0
- data/lib/support/active_support_lite/blank.rb +67 -0
- data/lib/support/active_support_lite/cattr_reader.rb +57 -0
- data/lib/support/active_support_lite/keys.rb +57 -0
- data/lib/support/active_support_lite/misc.rb +59 -0
- data/lib/support/active_support_lite/module.rb +1 -0
- data/lib/support/active_support_lite/module/delegation.rb +130 -0
- data/lib/support/active_support_lite/object.rb +9 -0
- data/lib/support/active_support_lite/string.rb +38 -0
- data/lib/support/active_support_lite/symbol.rb +16 -0
- data/lib/support/applicable.rb +41 -0
- data/lib/support/arrays.rb +197 -0
- data/lib/support/core_ext.rb +90 -0
- data/lib/support/exceptions.rb +106 -0
- data/lib/support/has_options.rb +16 -0
- data/lib/support/logger.rb +165 -0
- data/lib/support/methodical.rb +17 -0
- data/lib/support/no_stdout.rb +55 -0
- data/lib/support/plotter.rb +62 -0
- data/lib/support/vizier.rb +300 -0
- data/lib/tasks/spec_last.rake +55 -0
- data/lib/tasks/state_fu.rake +57 -0
- data/lib/transition.rb +338 -0
- data/lib/transition_query.rb +224 -0
- data/spec/custom_formatter.rb +49 -0
- data/spec/features/binding_and_transition_helper_mixin_spec.rb +111 -0
- data/spec/features/method_missing_only_once_spec.rb +28 -0
- data/spec/features/not_requirements_spec.rb +118 -0
- 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 +47 -0
- data/spec/features/transition_boolean_comparison_spec.rb +101 -0
- data/spec/helper.rb +13 -0
- data/spec/integration/active_record_persistence_spec.rb +202 -0
- data/spec/integration/binding_extension_spec.rb +41 -0
- data/spec/integration/class_accessor_spec.rb +117 -0
- data/spec/integration/event_definition_spec.rb +74 -0
- data/spec/integration/example_01_document_spec.rb +133 -0
- data/spec/integration/example_02_string_spec.rb +88 -0
- data/spec/integration/instance_accessor_spec.rb +97 -0
- data/spec/integration/lathe_extension_spec.rb +67 -0
- data/spec/integration/machine_duplication_spec.rb +101 -0
- data/spec/integration/relaxdb_persistence_spec.rb +97 -0
- data/spec/integration/requirement_reflection_spec.rb +270 -0
- data/spec/integration/state_definition_spec.rb +163 -0
- data/spec/integration/transition_spec.rb +1033 -0
- data/spec/spec.opts +9 -0
- data/spec/spec_helper.rb +132 -0
- data/spec/state_fu_spec.rb +948 -0
- data/spec/units/binding_spec.rb +192 -0
- data/spec/units/event_spec.rb +214 -0
- data/spec/units/exceptions_spec.rb +82 -0
- data/spec/units/lathe_spec.rb +570 -0
- data/spec/units/machine_spec.rb +229 -0
- data/spec/units/method_factory_spec.rb +366 -0
- data/spec/units/sprocket_spec.rb +69 -0
- data/spec/units/state_spec.rb +59 -0
- metadata +171 -0
data/lib/event.rb
ADDED
@@ -0,0 +1,192 @@
|
|
1
|
+
module StateFu
|
2
|
+
class Event < StateFu::Sprocket
|
3
|
+
|
4
|
+
attr_reader :origins, :targets, :requirements, :sequence
|
5
|
+
|
6
|
+
# called by Lathe when a new event is constructed
|
7
|
+
def initialize(machine, name, options={})
|
8
|
+
@requirements = [].extend ArrayWithSymbolAccessor
|
9
|
+
@sequence = {}
|
10
|
+
super( machine, name, options )
|
11
|
+
end
|
12
|
+
|
13
|
+
# Sequences: pretending events are state-local -
|
14
|
+
# probably a bad idea but here for a "compatibility mode"
|
15
|
+
# with eg the activemodel state machine
|
16
|
+
|
17
|
+
# build a hash of target => [origins]
|
18
|
+
def add_to_sequence origin_states, target_state
|
19
|
+
origin_states = [origin_states].flatten
|
20
|
+
existing = origin_states.select {|s| target_for_origin(s) }
|
21
|
+
raise ArgumentError.new unless existing.empty? && !targets
|
22
|
+
@sequence[target_state] ||= []
|
23
|
+
[origin_states].flatten.each do |o|
|
24
|
+
@sequence[target_state] << o
|
25
|
+
end
|
26
|
+
@sequence
|
27
|
+
end
|
28
|
+
|
29
|
+
def sequence?
|
30
|
+
!sequence.empty?
|
31
|
+
end
|
32
|
+
|
33
|
+
def target_for_origin origin_state
|
34
|
+
raise ArgumentError.new if origin_state.nil?
|
35
|
+
name = sequence.detect do |k,v|
|
36
|
+
v.include?(origin_state.to_sym)
|
37
|
+
end[0] rescue nil
|
38
|
+
machine.states[name] if name
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def can_transition_from?(origin_state)
|
43
|
+
( origins && origins.include?(origin_state.to_sym) && !targets.blank?) ||
|
44
|
+
target_for_origin(origin_state)
|
45
|
+
end
|
46
|
+
|
47
|
+
# the names of all possible origin states
|
48
|
+
def origin_names
|
49
|
+
origins ? origins.map(&:to_sym) : nil
|
50
|
+
end
|
51
|
+
|
52
|
+
# the names of all possible target states
|
53
|
+
def target_names
|
54
|
+
targets ? targets.map(&:to_sym) : nil
|
55
|
+
end
|
56
|
+
|
57
|
+
# tests if a state or state name is in the list of targets
|
58
|
+
def to?( state )
|
59
|
+
target_names.include?( state.to_sym )
|
60
|
+
end
|
61
|
+
|
62
|
+
# tests if a state or state name is in the list of origins
|
63
|
+
def from?( state )
|
64
|
+
origin_names.include?( state.to_sym ) || target_for_origin(state)
|
65
|
+
end
|
66
|
+
|
67
|
+
def cycle?
|
68
|
+
origin && (origin == target)
|
69
|
+
end
|
70
|
+
|
71
|
+
# *adds to* the origin states given a list of symbols / States
|
72
|
+
def origins=( *args )
|
73
|
+
update_state_collection( '@origins', *args )
|
74
|
+
end
|
75
|
+
|
76
|
+
# *adds to* the target states given a list of symbols / States
|
77
|
+
def targets=( *args )
|
78
|
+
update_state_collection( '@targets', *args )
|
79
|
+
end
|
80
|
+
|
81
|
+
# if there is a single state in #origins, returns it
|
82
|
+
def origin
|
83
|
+
origins && origins.length == 1 && origins[0] || nil
|
84
|
+
end
|
85
|
+
|
86
|
+
# if there is a single state in #origins, returns it
|
87
|
+
def target
|
88
|
+
targets && targets.length == 1 && targets[0] || nil
|
89
|
+
end
|
90
|
+
|
91
|
+
# a simple event has exactly one target, and any number of
|
92
|
+
# origins. It's simple because it can be triggered without
|
93
|
+
# supplying a target name - ie, <tt>go!<tt> vs <tt>go!(:home)<tt>
|
94
|
+
def simple?
|
95
|
+
!! ( origins && target || sequence? )
|
96
|
+
end
|
97
|
+
|
98
|
+
def fireable?( transition )
|
99
|
+
transition.valid?(true)
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
#
|
104
|
+
# Lathe methods
|
105
|
+
#
|
106
|
+
|
107
|
+
# adds an event requirement.
|
108
|
+
# DOCME // TODO - can this be removed?
|
109
|
+
def requires( *args, &block )
|
110
|
+
lathe.requires( *args, &block )
|
111
|
+
end
|
112
|
+
|
113
|
+
# generally called from a Lathe. Sets the origin(s) and optionally
|
114
|
+
# target(s) - that is, if you supply the :to option, or a single element
|
115
|
+
# hash of origins => targets ) of the event. Both origins= and
|
116
|
+
# targets= are accumulators.
|
117
|
+
def from *args
|
118
|
+
options = args.extract_options!.symbolize_keys!
|
119
|
+
args.flatten!
|
120
|
+
to = options.delete(:to) || options.delete(:transitions_to)
|
121
|
+
if args.empty? && !to
|
122
|
+
if options.length == 1
|
123
|
+
self.origins = options.keys[0]
|
124
|
+
self.targets = options.values[0]
|
125
|
+
else
|
126
|
+
raise options.inspect
|
127
|
+
end
|
128
|
+
else
|
129
|
+
self.origins = *args
|
130
|
+
self.targets = to unless to.nil?
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# sets the target states for the event.
|
135
|
+
def to *args
|
136
|
+
options = args.extract_options!.symbolize_keys!
|
137
|
+
args.flatten!
|
138
|
+
raise options.inspect unless options.empty?
|
139
|
+
self.targets= *args
|
140
|
+
end
|
141
|
+
|
142
|
+
alias_method :transitions_to, :to
|
143
|
+
alias_method :transitions_from, :from
|
144
|
+
|
145
|
+
#
|
146
|
+
# misc
|
147
|
+
#
|
148
|
+
|
149
|
+
# display nice and short
|
150
|
+
def inspect
|
151
|
+
s = self.to_s
|
152
|
+
s = s[0,s.length-1]
|
153
|
+
display_hooks = hooks.dup
|
154
|
+
display_hooks.each do |k,v|
|
155
|
+
display_hooks.delete(k) if v.empty?
|
156
|
+
end
|
157
|
+
unless display_hooks.empty?
|
158
|
+
s << " hooks=#{display_hooks.inspect}"
|
159
|
+
end
|
160
|
+
unless requirements.empty?
|
161
|
+
s << " requirements=#{requirements.inspect}"
|
162
|
+
end
|
163
|
+
s << " targets=#{targets.map(&:to_sym).inspect}" if targets
|
164
|
+
s << " origins=#{origins.map(&:to_sym).inspect}" if origins
|
165
|
+
s << ">"
|
166
|
+
s
|
167
|
+
end
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
# internal method which accumulates states into an instance
|
172
|
+
# variable with successive invocations.
|
173
|
+
# ensures that calling #from multiple times adds to, rather than
|
174
|
+
# clobbering, the list of origins / targets.
|
175
|
+
def update_state_collection( ivar_name, *args)
|
176
|
+
raise ArgumentError if sequence?
|
177
|
+
new_states = if [args].flatten == [:ALL]
|
178
|
+
machine.states
|
179
|
+
else
|
180
|
+
machine.find_or_create_states_by_name( *args.flatten )
|
181
|
+
end
|
182
|
+
unless new_states.is_a?( Array )
|
183
|
+
new_states = [new_states]
|
184
|
+
end
|
185
|
+
existing = instance_variable_get( ivar_name )
|
186
|
+
# return existing if new_states.empty?
|
187
|
+
new_value = ((existing || [] ) + new_states).flatten.compact.uniq.extend( StateArray )
|
188
|
+
instance_variable_set( ivar_name, new_value )
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
end
|
data/lib/executioner.rb
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
module StateFu
|
2
|
+
#
|
3
|
+
# delegator class for evaluation methods / procs in the context of
|
4
|
+
# your object.
|
5
|
+
#
|
6
|
+
|
7
|
+
class Executioner
|
8
|
+
|
9
|
+
# give us a blank slate
|
10
|
+
# instance_methods.each { |m| undef_method m unless m =~ /(^__|^self|^nil\?$|^send$|proxy_|^object_id|^respond_to\?|^instance_exec|^instance_eval|^method$)/ }
|
11
|
+
|
12
|
+
def initialize transition, &block
|
13
|
+
@transition = transition
|
14
|
+
@__target__ = transition.object
|
15
|
+
@__self___ = self
|
16
|
+
yield self if block_given?
|
17
|
+
# forces method_missing to snap back to its pre-state-fu condition:
|
18
|
+
# @__target__.initialize_state_fu!
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
delegate :origin, :to => :transition, :prefix => true # transition_origin
|
23
|
+
delegate :target, :to => :transition, :prefix => true # transition_target
|
24
|
+
delegate :event, :to => :transition, :prefix => true # transition_event
|
25
|
+
|
26
|
+
delegate :halt!, :to => :transition
|
27
|
+
delegate :args, :to => :transition
|
28
|
+
delegate :options, :to => :transition
|
29
|
+
|
30
|
+
def binding
|
31
|
+
transition.binding
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :transition, :__target__, :__self__
|
35
|
+
|
36
|
+
alias_method :t, :transition
|
37
|
+
alias_method :current_transition, :transition
|
38
|
+
alias_method :context, :transition
|
39
|
+
alias_method :ctx, :transition
|
40
|
+
|
41
|
+
alias_method :arguments, :args
|
42
|
+
alias_method :transition_arguments, :args
|
43
|
+
|
44
|
+
def machine
|
45
|
+
binding.machine
|
46
|
+
end
|
47
|
+
|
48
|
+
def states
|
49
|
+
machine.states
|
50
|
+
end
|
51
|
+
# delegate :machine, :to => :transition
|
52
|
+
|
53
|
+
def evaluate_with_arguments method_name_or_proc, *arguments
|
54
|
+
if method_name_or_proc.is_a?(Proc) && meth = method_name_or_proc
|
55
|
+
elsif meth = transition.machine.named_procs[method_name_or_proc]
|
56
|
+
elsif respond_to?( method_name_or_proc) && meth = method(method_name_or_proc)
|
57
|
+
elsif method_name_or_proc.to_s =~ /^not?_(.*)$/
|
58
|
+
# special case: prefix a method with no_ or not_ and get the
|
59
|
+
# boolean opposite of its evaluation result
|
60
|
+
return !( evaluate_with_arguments $1, *args )
|
61
|
+
else
|
62
|
+
raise NoMethodError.new( "undefined method_name `#{method_name_or_proc.to_s}' for \"#{__target__}\":#{__target__.class.to_s}" )
|
63
|
+
end
|
64
|
+
|
65
|
+
if arguments.length < meth.arity.abs && meth.arity != -1
|
66
|
+
# ensure we don't have too few arguments
|
67
|
+
raise ArgumentError.new([meth.arity, arguments.length].inspect)
|
68
|
+
else
|
69
|
+
# ensure we don't pass too many arguments
|
70
|
+
arguments = arguments[0, meth.arity.abs]
|
71
|
+
end
|
72
|
+
|
73
|
+
# execute it!
|
74
|
+
__target__.with_methods_on(self) do
|
75
|
+
self.instance_exec *arguments, &meth
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def evaluate method_name_or_proc
|
80
|
+
arguments = [transition, args, __target__]
|
81
|
+
evaluate_with_arguments(method_name_or_proc, *arguments)
|
82
|
+
end
|
83
|
+
|
84
|
+
alias_method :executioner_respond_to?, :respond_to?
|
85
|
+
|
86
|
+
def respond_to? method_name, include_private = false
|
87
|
+
executioner_respond_to?(method_name, include_private) ||
|
88
|
+
__target__.__send__( :respond_to?, method_name, include_private )
|
89
|
+
end
|
90
|
+
|
91
|
+
alias_method :executioner_method, :method
|
92
|
+
def method method_name
|
93
|
+
begin
|
94
|
+
executioner_method(method_name)
|
95
|
+
rescue NameError
|
96
|
+
__target__.__send__ :method, method_name
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
# Forwards any missing method call to the \target.
|
103
|
+
# TODO / NOTE: we don't (can't ?) handle block arguments ...
|
104
|
+
def method_missing(method_name, *args)
|
105
|
+
if __target__.respond_to?(method_name, true)
|
106
|
+
begin
|
107
|
+
meth = __target__.__send__ :method, method_name
|
108
|
+
rescue NameError
|
109
|
+
super
|
110
|
+
end
|
111
|
+
__target__.instance_exec( *args, &meth)
|
112
|
+
else # let's hope it's a named proc
|
113
|
+
evaluate_with_arguments(method_name, *args)
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
# NOTE: const_missing is not handled.
|
119
|
+
end
|
120
|
+
end
|
data/lib/hooks.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
module StateFu
|
2
|
+
|
3
|
+
# TODO document structure / sequence of hooks elsewhere
|
4
|
+
|
5
|
+
module Hooks #:nodoc
|
6
|
+
|
7
|
+
ALL_HOOKS = [[:machine, :before_all], # global before. prepare for any transition
|
8
|
+
[:event, :before], # prepare for the event
|
9
|
+
[:origin, :exit], # say goodbye!
|
10
|
+
[:event, :execute], # do stuff here for the event
|
11
|
+
[:target, :entry], # entry point. last chance to halt!
|
12
|
+
[:event, :after], # clean up after transition
|
13
|
+
[:target, :accepted], # state is changed. Do something about it.
|
14
|
+
[:machine, :after_all]] # global after. close up shop.
|
15
|
+
|
16
|
+
EVENT_HOOKS = ALL_HOOKS.select { |type, name| type == :event }
|
17
|
+
STATE_HOOKS = ALL_HOOKS.select { |type, name| [:origin, :target].include?(type) }
|
18
|
+
MACHINE_HOOKS = ALL_HOOKS.select { |type, name| type == :machine }
|
19
|
+
HOOK_NAMES = ALL_HOOKS.map(&:last)
|
20
|
+
|
21
|
+
# just turn the above into what each class needs
|
22
|
+
# and make it into a nice hash: { :name =>[ hook, ... ], ... }
|
23
|
+
def self.for( instance )
|
24
|
+
case instance
|
25
|
+
when State
|
26
|
+
STATE_HOOKS
|
27
|
+
when Event
|
28
|
+
EVENT_HOOKS
|
29
|
+
when Machine
|
30
|
+
MACHINE_HOOKS
|
31
|
+
when Sprocket
|
32
|
+
[]
|
33
|
+
end.
|
34
|
+
map { |type, name| [name, [].extend( OrderedHash )] }.
|
35
|
+
to_h.extend( OrderedHash ).freeze
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
data/lib/interface.rb
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
module StateFu
|
2
|
+
module Interface
|
3
|
+
module SoftAlias
|
4
|
+
|
5
|
+
# define aliases that won't clobber existing methods -
|
6
|
+
# so we can be liberal with them.
|
7
|
+
def soft_alias(x)
|
8
|
+
aliases = [ x.to_a[0] ].flatten
|
9
|
+
original = aliases.shift
|
10
|
+
existing_method_names = (self.instance_methods | self.protected_instance_methods | self.private_instance_methods).map(&:to_sym)
|
11
|
+
taken, ok = aliases.partition { |a| existing_method_names.include?(a.to_sym) }
|
12
|
+
StateFu::Logger.debug("#{self.to_s} alias for ## #{original} already taken: #{taken.inspect}") unless taken.empty?
|
13
|
+
ok.each { |a| alias_method a, original}
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
module Aliases
|
19
|
+
|
20
|
+
def self.extended(base)
|
21
|
+
base.extend SoftAlias
|
22
|
+
base.class_eval do
|
23
|
+
# instance method aliases
|
24
|
+
soft_alias :state_fu => [:stfu, :fu, :stateful, :workflow, :engine, :machine, :context]
|
25
|
+
soft_alias :state_fu_bindings => [:bindings, :workflows, :engines, :machines, :contexts]
|
26
|
+
soft_alias :state_fu! => [:stfu!, :initialize_machines!, :initialize_state!]
|
27
|
+
class << self
|
28
|
+
extend SoftAlias
|
29
|
+
# class method aliases
|
30
|
+
soft_alias :state_fu_machine => [:stfu, :state_fu, :workflow, :stateful, :statefully, :state_machine, :engine]
|
31
|
+
soft_alias :state_fu_machines => [:stfus, :state_fus, :workflows, :engines]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Provides access to StateFu to your classes. Plenty of aliases are
|
38
|
+
# provided so you can use whatever makes sense to you.
|
39
|
+
module ClassMethods
|
40
|
+
|
41
|
+
# TODO:
|
42
|
+
# take option :alias => false (disable aliases) or :alias
|
43
|
+
# => :foo (add :foo as class & instance accessor methods)
|
44
|
+
|
45
|
+
# Given no arguments, return the default machine (:state_fu) for the
|
46
|
+
# class, creating it if it did not exist.
|
47
|
+
#
|
48
|
+
# Given a symbol, return the machine by that name, creating it
|
49
|
+
# if it didn't exist, and definining it if a block is passed.
|
50
|
+
#
|
51
|
+
# Given a block, apply it to a StateFu::Lathe to define a
|
52
|
+
# machine, and return it.
|
53
|
+
#
|
54
|
+
# This can be done multiple times; changes are cumulative.
|
55
|
+
#
|
56
|
+
# You can have as many machines as you like per class.
|
57
|
+
#
|
58
|
+
# Klass.machine # the default machine named :om
|
59
|
+
# # equivalent to Klass.machine(:om)
|
60
|
+
# Klass.machine(:workflow) # another totally separate machine
|
61
|
+
#
|
62
|
+
# recognised options are:
|
63
|
+
# :field_name - specify the field to use for persistence.
|
64
|
+
# defaults to {machine_name}_field.
|
65
|
+
#
|
66
|
+
def state_fu_machine( *args, &block )
|
67
|
+
options = args.extract_options!.symbolize_keys!
|
68
|
+
name = args[0] || DEFAULT
|
69
|
+
StateFu::Machine.for_class( self, name, options, &block )
|
70
|
+
end
|
71
|
+
alias_method :machine, :state_fu_machine
|
72
|
+
|
73
|
+
def state_fu_field_names
|
74
|
+
@_state_fu_field_names ||= {}
|
75
|
+
end
|
76
|
+
|
77
|
+
def state_fu_machines
|
78
|
+
@_state_fu_machines ||= {}
|
79
|
+
end
|
80
|
+
alias_method :machines, :state_fu_machines
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
# These methods grant access to StateFu::Binding objects, which
|
85
|
+
# are bundles of context encapsulating a StateFu::Machine, an instance
|
86
|
+
# of a class, and its current state in the machine.
|
87
|
+
|
88
|
+
module InstanceMethods
|
89
|
+
|
90
|
+
def state_fu_bindings
|
91
|
+
@_state_fu_bindings ||= {}
|
92
|
+
end
|
93
|
+
|
94
|
+
# A StateFu::Binding comes into being when it is first referenced.
|
95
|
+
#
|
96
|
+
# This is the accessor method through which an object instance (or developer)
|
97
|
+
# can access a StateFu::Machine, the object's current state, the
|
98
|
+
# methods which trigger event transitions, etc.
|
99
|
+
|
100
|
+
def state_fu_binding( name = DEFAULT )
|
101
|
+
name = name.to_sym
|
102
|
+
if machine = self.class.state_fu_machines[name]
|
103
|
+
state_fu_bindings[name] ||= StateFu::Binding.new( machine, self, name )
|
104
|
+
else raise ArgumentError.new("No state machine called #{name} for #{self.class} #{self}")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
alias_method :state_fu, :state_fu_binding
|
108
|
+
|
109
|
+
def current_state( name = DEFAULT )
|
110
|
+
state_fu_binding(name).current_state
|
111
|
+
end
|
112
|
+
|
113
|
+
def next!(name = DEFAULT, *args, &block )
|
114
|
+
state_fu_binding(name).next! *args, &block
|
115
|
+
end
|
116
|
+
alias_method :next_state!, :next!
|
117
|
+
alias_method :fire_next_transition!, :next!
|
118
|
+
|
119
|
+
# Instantiate bindings for all machines, which ensures that persistence
|
120
|
+
# fields are intialized and event methods defined.
|
121
|
+
# It's useful to call this before_create w/
|
122
|
+
# ActiveRecord classes, as this will cause the database field
|
123
|
+
# to be populated with the default state name.
|
124
|
+
|
125
|
+
def state_fu!
|
126
|
+
MethodFactory.define_singleton_method(self, :initialize_state_fu!) { true }
|
127
|
+
self.class.state_fu_machines.keys.map { |n| state_fu_binding( n ) }
|
128
|
+
end
|
129
|
+
|
130
|
+
end # ClassMethods
|
131
|
+
end # Interface
|
132
|
+
end # StateFu
|