davidlee-state-fu 0.0.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 +174 -0
- data/Rakefile +87 -0
- data/lib/no_stdout.rb +32 -0
- data/lib/state-fu.rb +93 -0
- data/lib/state_fu/binding.rb +262 -0
- data/lib/state_fu/core_ext.rb +23 -0
- data/lib/state_fu/event.rb +98 -0
- data/lib/state_fu/exceptions.rb +42 -0
- data/lib/state_fu/fu_space.rb +50 -0
- data/lib/state_fu/helper.rb +189 -0
- data/lib/state_fu/hooks.rb +28 -0
- data/lib/state_fu/interface.rb +139 -0
- data/lib/state_fu/lathe.rb +247 -0
- data/lib/state_fu/logger.rb +10 -0
- data/lib/state_fu/machine.rb +159 -0
- data/lib/state_fu/method_factory.rb +95 -0
- data/lib/state_fu/persistence/active_record.rb +27 -0
- data/lib/state_fu/persistence/attribute.rb +46 -0
- data/lib/state_fu/persistence/base.rb +98 -0
- data/lib/state_fu/persistence/session.rb +7 -0
- data/lib/state_fu/persistence.rb +50 -0
- data/lib/state_fu/sprocket.rb +27 -0
- data/lib/state_fu/state.rb +45 -0
- data/lib/state_fu/transition.rb +213 -0
- data/spec/helper.rb +86 -0
- data/spec/integration/active_record_persistence_spec.rb +189 -0
- data/spec/integration/class_accessor_spec.rb +127 -0
- data/spec/integration/event_definition_spec.rb +74 -0
- data/spec/integration/ex_machine_for_accounts_spec.rb +79 -0
- data/spec/integration/example_01_document_spec.rb +127 -0
- data/spec/integration/example_02_string_spec.rb +87 -0
- data/spec/integration/instance_accessor_spec.rb +100 -0
- data/spec/integration/machine_duplication_spec.rb +95 -0
- data/spec/integration/requirement_reflection_spec.rb +201 -0
- data/spec/integration/sanity_spec.rb +31 -0
- data/spec/integration/state_definition_spec.rb +177 -0
- data/spec/integration/transition_spec.rb +1060 -0
- data/spec/spec.opts +7 -0
- data/spec/units/binding_spec.rb +145 -0
- data/spec/units/event_spec.rb +232 -0
- data/spec/units/exceptions_spec.rb +75 -0
- data/spec/units/fu_space_spec.rb +95 -0
- data/spec/units/lathe_spec.rb +567 -0
- data/spec/units/machine_spec.rb +237 -0
- data/spec/units/method_factory_spec.rb +359 -0
- data/spec/units/sprocket_spec.rb +71 -0
- data/spec/units/state_spec.rb +50 -0
- metadata +122 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
module StateFu
|
|
2
|
+
class Lathe
|
|
3
|
+
|
|
4
|
+
# NOTE: Sprocket is the abstract superclass of Event and State
|
|
5
|
+
|
|
6
|
+
attr_reader :machine, :sprocket, :options
|
|
7
|
+
|
|
8
|
+
def initialize( machine, sprocket = nil, options={}, &block )
|
|
9
|
+
@machine = machine
|
|
10
|
+
@sprocket = sprocket
|
|
11
|
+
@options = options.symbolize_keys!
|
|
12
|
+
if sprocket
|
|
13
|
+
sprocket.apply!( options )
|
|
14
|
+
end
|
|
15
|
+
if block_given?
|
|
16
|
+
if block.arity == 1
|
|
17
|
+
if sprocket
|
|
18
|
+
yield sprocket
|
|
19
|
+
else
|
|
20
|
+
raise ArgumentError
|
|
21
|
+
end
|
|
22
|
+
else
|
|
23
|
+
instance_eval( &block )
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# a 'child' lathe is created by apply_to, to deal with nested
|
|
31
|
+
# blocks for states / events ( which are sprockets )
|
|
32
|
+
def child?
|
|
33
|
+
!!@sprocket
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# is this the toplevel lathe for a machine?
|
|
37
|
+
def master?
|
|
38
|
+
!child?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# instantiate a child lathe and apply the given block
|
|
42
|
+
def apply_to( sprocket, options, &block )
|
|
43
|
+
StateFu::Lathe.new( machine, sprocket, options, &block )
|
|
44
|
+
sprocket
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# require that the current sprocket be of a given type
|
|
48
|
+
def require_sprocket( *valid_types )
|
|
49
|
+
raise ArgumentError.new("Lathe is for a #{sprocket.class}, not one of #{valid_types.inspect}") unless valid_types.include?( sprocket.class )
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# ensure this is not a child lathe
|
|
53
|
+
def require_no_sprocket()
|
|
54
|
+
require_sprocket( NilClass )
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# abstract method for defining states / events
|
|
58
|
+
def define_sprocket( type, name, options={}, &block )
|
|
59
|
+
name = name.to_sym
|
|
60
|
+
klass = StateFu.const_get((a=type.to_s.split('',2);[a.first.upcase, a.last].join))
|
|
61
|
+
collection = machine.send("#{type}s")
|
|
62
|
+
options.symbolize_keys!
|
|
63
|
+
if sprocket = collection[name]
|
|
64
|
+
apply_to( sprocket, options, &block )
|
|
65
|
+
sprocket
|
|
66
|
+
else
|
|
67
|
+
sprocket = klass.new( machine, name, options )
|
|
68
|
+
collection << sprocket
|
|
69
|
+
apply_to( sprocket, options, &block )
|
|
70
|
+
sprocket
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def define_state( name, options={}, &block )
|
|
75
|
+
define_sprocket( :state, name, options, &block )
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def define_event( name, options={}, &block )
|
|
79
|
+
define_sprocket( :event, name, options, &block )
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def define_hook slot, method_name=nil, &block
|
|
83
|
+
unless sprocket.hooks.has_key?( slot )
|
|
84
|
+
raise ArgumentError, "invalid hook type #{slot.inspect} for #{sprocket.class}"
|
|
85
|
+
end
|
|
86
|
+
if block_given?
|
|
87
|
+
# unless (-1..1).include?( block.arity )
|
|
88
|
+
# raise ArgumentError, "unexpected block arity: #{block.arity}"
|
|
89
|
+
# end
|
|
90
|
+
case method_name
|
|
91
|
+
when Symbol
|
|
92
|
+
machine.named_procs[method_name] = block
|
|
93
|
+
hook = method_name
|
|
94
|
+
when NilClass
|
|
95
|
+
hook = block
|
|
96
|
+
# allow only one anonymous hook per slot in the interests of
|
|
97
|
+
# sanity - replace any pre-existing ones
|
|
98
|
+
sprocket.hooks[slot].delete_if { |h| Proc === h }
|
|
99
|
+
else
|
|
100
|
+
raise ArgumentError.new( method_name.inspect )
|
|
101
|
+
end
|
|
102
|
+
elsif method_name.is_a?( Symbol ) # no block
|
|
103
|
+
hook = method_name
|
|
104
|
+
# prevent duplicates
|
|
105
|
+
sprocket.hooks[slot].delete_if { |h| hook == h }
|
|
106
|
+
else
|
|
107
|
+
raise ArgumentError, "#{method_name.class} is not a symbol"
|
|
108
|
+
end
|
|
109
|
+
sprocket.hooks[slot] << hook
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
public
|
|
113
|
+
|
|
114
|
+
# helpers are mixed into all binding / transition contexts
|
|
115
|
+
def helper( *modules )
|
|
116
|
+
machine.helper *modules
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
#
|
|
120
|
+
# event definition
|
|
121
|
+
#
|
|
122
|
+
|
|
123
|
+
def event( name, options={}, &block )
|
|
124
|
+
options.symbolize_keys!
|
|
125
|
+
require_sprocket( StateFu::State, NilClass )
|
|
126
|
+
if child? && sprocket.is_a?( StateFu::State ) # in state block
|
|
127
|
+
targets = options.delete(:to)
|
|
128
|
+
evt = define_event( name, options, &block )
|
|
129
|
+
evt.from sprocket unless evt.origins
|
|
130
|
+
evt.to( targets ) unless targets.nil?
|
|
131
|
+
evt
|
|
132
|
+
else # in master lathe
|
|
133
|
+
origins = options.delete( :from )
|
|
134
|
+
targets = options.delete( :to )
|
|
135
|
+
evt = define_event( name, options, &block )
|
|
136
|
+
evt.from origins unless origins.nil?
|
|
137
|
+
evt.to targets unless targets.nil?
|
|
138
|
+
evt
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def requires( *args, &block )
|
|
143
|
+
require_sprocket( StateFu::Event, StateFu::State )
|
|
144
|
+
options = args.extract_options!.symbolize_keys!
|
|
145
|
+
options.assert_valid_keys(:on, :message, :msg )
|
|
146
|
+
names = args
|
|
147
|
+
if block_given? && args.length > 1
|
|
148
|
+
raise ArgumentError.new("cannot supply a block for multiple requirements")
|
|
149
|
+
end
|
|
150
|
+
on = nil
|
|
151
|
+
names.each do |name|
|
|
152
|
+
raise ArgumentError.new( name.inspect ) unless name.is_a?( Symbol )
|
|
153
|
+
case sprocket
|
|
154
|
+
when StateFu::State
|
|
155
|
+
on ||= [(options.delete(:on) || [:entry])].flatten
|
|
156
|
+
sprocket.entry_requirements << name if on.include?( :entry )
|
|
157
|
+
sprocket.exit_requirements << name if on.include?( :exit )
|
|
158
|
+
when StateFu::Event
|
|
159
|
+
sprocket.requirements << name
|
|
160
|
+
end
|
|
161
|
+
if block_given?
|
|
162
|
+
machine.named_procs[name] = block
|
|
163
|
+
end
|
|
164
|
+
if msg = options.delete(:message) || options.delete(:msg)
|
|
165
|
+
# TODO - move this into machine
|
|
166
|
+
raise ArgumentError, msg.inspect unless [String, Symbol, Proc].include?(msg.class)
|
|
167
|
+
machine.requirement_messages[name] = msg
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
alias_method :must, :requires
|
|
172
|
+
alias_method :must_be, :requires
|
|
173
|
+
alias_method :needs, :requires
|
|
174
|
+
alias_method :satisfy, :requires
|
|
175
|
+
alias_method :must_satisfy, :requires
|
|
176
|
+
|
|
177
|
+
# create an event from *and* to the current state.
|
|
178
|
+
# Creates a loop, useful (only) for hooking behaviours onto.
|
|
179
|
+
def cycle( name=nil, options={}, &block )
|
|
180
|
+
name ||= "cycle_#{sprocket.name.to_s}"
|
|
181
|
+
require_sprocket( StateFu::State )
|
|
182
|
+
evt = define_event( name, options, &block )
|
|
183
|
+
evt.from sprocket
|
|
184
|
+
evt.to sprocket
|
|
185
|
+
evt
|
|
186
|
+
# raise NotImplementedError
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
#
|
|
190
|
+
# state definition
|
|
191
|
+
#
|
|
192
|
+
|
|
193
|
+
def initial_state( *args, &block )
|
|
194
|
+
require_no_sprocket()
|
|
195
|
+
machine.initial_state= state( *args, &block)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def state( name, options={}, &block )
|
|
199
|
+
require_no_sprocket()
|
|
200
|
+
define_state( name, options, &block )
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def from *args, &block
|
|
204
|
+
require_sprocket( StateFu::Event )
|
|
205
|
+
sprocket.from( *args, &block )
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def to *args, &block
|
|
209
|
+
require_sprocket( StateFu::Event )
|
|
210
|
+
sprocket.to( *args, &block )
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
#
|
|
214
|
+
# do something with all states / events
|
|
215
|
+
#
|
|
216
|
+
def each_sprocket( type, *args, &block)
|
|
217
|
+
require_no_sprocket()
|
|
218
|
+
options = args.extract_options!.symbolize_keys!
|
|
219
|
+
if args == [:ALL] || args == []
|
|
220
|
+
args = machine.send("#{type}s").except( options.delete(:except) )
|
|
221
|
+
end
|
|
222
|
+
args.map { |name| self.send( type, name, options.dup, &block) }.extend StateArray
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def states( *args, &block )
|
|
226
|
+
each_sprocket( 'state', *args, &block )
|
|
227
|
+
end
|
|
228
|
+
alias_method :all_states, :states
|
|
229
|
+
alias_method :each_state, :states
|
|
230
|
+
|
|
231
|
+
def events( *args, &block )
|
|
232
|
+
each_sprocket( 'event', *args, &block )
|
|
233
|
+
end
|
|
234
|
+
alias_method :all_events, :events
|
|
235
|
+
alias_method :each_event, :events
|
|
236
|
+
|
|
237
|
+
# Bunch of silly little methods for defining events
|
|
238
|
+
|
|
239
|
+
def before *a, &b; require_sprocket( StateFu::Event ); define_hook :before, *a, &b; end
|
|
240
|
+
def on_exit *a, &b; require_sprocket( StateFu::State ); define_hook :exit, *a, &b; end
|
|
241
|
+
def execute *a, &b; require_sprocket( StateFu::Event ); define_hook :execute, *a, &b; end
|
|
242
|
+
def on_entry *a, &b; require_sprocket( StateFu::State ); define_hook :entry, *a, &b; end
|
|
243
|
+
def after *a, &b; require_sprocket( StateFu::Event ); define_hook :after, *a, &b; end
|
|
244
|
+
def accepted *a, &b; require_sprocket( StateFu::State ); define_hook :accepted, *a, &b; end
|
|
245
|
+
|
|
246
|
+
end
|
|
247
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
module StateFu
|
|
2
|
+
class Machine
|
|
3
|
+
include Helper
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# meta-constructor; expects to be called via Klass.machine()
|
|
7
|
+
def self.for_class(klass, name, options={}, &block)
|
|
8
|
+
options.symbolize_keys!
|
|
9
|
+
name = name.to_sym
|
|
10
|
+
unless machine = StateFu::FuSpace.class_machines[ klass ][ name ]
|
|
11
|
+
machine = new( name, options, &block )
|
|
12
|
+
machine.bind!( klass, name, options[:field_name] )
|
|
13
|
+
end
|
|
14
|
+
if block_given?
|
|
15
|
+
machine.apply!( &block )
|
|
16
|
+
end
|
|
17
|
+
machine
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
## Instance Methods
|
|
22
|
+
##
|
|
23
|
+
|
|
24
|
+
attr_reader :states, :events, :options, :helpers, :named_procs, :requirement_messages, :name
|
|
25
|
+
|
|
26
|
+
def initialize( name, options={}, &block )
|
|
27
|
+
# TODO - @name isn't actually used anywhere yet except
|
|
28
|
+
# in deep_clone - remove it?
|
|
29
|
+
@name = name
|
|
30
|
+
@states = [].extend( StateArray )
|
|
31
|
+
@events = [].extend( EventArray )
|
|
32
|
+
@helpers = [].extend( HelperArray )
|
|
33
|
+
@named_procs = {}
|
|
34
|
+
@requirement_messages = {}
|
|
35
|
+
@options = options
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def deep_clone()
|
|
39
|
+
m = Machine.new( name, options.dup )
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# merge the commands in &block with the existing machine; returns
|
|
43
|
+
# a lathe for the machine.
|
|
44
|
+
def apply!( &block )
|
|
45
|
+
StateFu::Lathe.new( self, &block )
|
|
46
|
+
end
|
|
47
|
+
alias_method :lathe, :apply!
|
|
48
|
+
|
|
49
|
+
def helper_modules
|
|
50
|
+
helpers.map do |h|
|
|
51
|
+
case h
|
|
52
|
+
when String, Symbol
|
|
53
|
+
Object.const_get( h.to_s.classify )
|
|
54
|
+
when Module
|
|
55
|
+
h
|
|
56
|
+
else
|
|
57
|
+
raise ArgumentError.new( h.class.inspect )
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def inject_helpers_into( obj )
|
|
63
|
+
metaclass = class << obj; self; end
|
|
64
|
+
|
|
65
|
+
mods = helper_modules()
|
|
66
|
+
metaclass.class_eval do
|
|
67
|
+
mods.each do |mod|
|
|
68
|
+
include( mod )
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# the modules listed here will be mixed into Binding and
|
|
74
|
+
# Transition objects for this machine. use this to define methods,
|
|
75
|
+
# references or data useful to you during transitions, event
|
|
76
|
+
# hooks, or in general use of StateFu.
|
|
77
|
+
#
|
|
78
|
+
# They can be supplied as a string/symbol (as per rails controller
|
|
79
|
+
# helpers), or a Module.
|
|
80
|
+
#
|
|
81
|
+
# To do this globally, just duck-punch StateFu::Machine /
|
|
82
|
+
# StateFu::Binding.
|
|
83
|
+
def helper *modules_to_add
|
|
84
|
+
modules_to_add.each { |mod| helpers << mod }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# make it so a class which has included StateFu has a binding to
|
|
88
|
+
# this machine
|
|
89
|
+
def bind!( klass, name=StateFu::DEFAULT_MACHINE, field_name = nil )
|
|
90
|
+
field_name ||= name.to_s.underscore.tr(' ', '_') + StateFu::Persistence::DEFAULT_FIELD_NAME_SUFFIX
|
|
91
|
+
field_name = field_name.to_sym
|
|
92
|
+
StateFu::FuSpace.insert!( klass, self, name, field_name )
|
|
93
|
+
# define an accessor method with the given name
|
|
94
|
+
unless name == StateFu::DEFAULT_MACHINE
|
|
95
|
+
klass.class_eval do
|
|
96
|
+
define_method name do
|
|
97
|
+
state_fu( name )
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# method_missing to catch NoMethodError for event methods, etc
|
|
103
|
+
StateFu::MethodFactory.prepare_class( klass )
|
|
104
|
+
|
|
105
|
+
# prepare the persistence field
|
|
106
|
+
StateFu::Persistence.prepare_field( klass, field_name )
|
|
107
|
+
true
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def empty?
|
|
111
|
+
states.empty?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def initial_state=( s )
|
|
115
|
+
case s
|
|
116
|
+
when Symbol, String, StateFu::State
|
|
117
|
+
unless init_state = states[ s.to_sym ]
|
|
118
|
+
init_state = StateFu::State.new( self, s.to_sym )
|
|
119
|
+
states << init_state
|
|
120
|
+
end
|
|
121
|
+
@initial_state = init_state
|
|
122
|
+
else
|
|
123
|
+
raise( ArgumentError, s.inspect )
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def initial_state()
|
|
128
|
+
@initial_state ||= states.first
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def state_names
|
|
132
|
+
states.map(&:name)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def event_names
|
|
136
|
+
events.map(&:name)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# given a messy bunch of symbols, find or create a list of
|
|
140
|
+
# matching States.
|
|
141
|
+
def find_or_create_states_by_name( *args )
|
|
142
|
+
args = args.compact.flatten
|
|
143
|
+
raise ArgumentError.new( args.inspect ) unless args.all? { |a| [Symbol, StateFu::State].include? a.class }
|
|
144
|
+
args.map do |s|
|
|
145
|
+
unless state = states[s.to_sym]
|
|
146
|
+
# TODO clean this line up
|
|
147
|
+
state = s.is_a?( StateFu::State ) ? s : StateFu::State.new( self, s )
|
|
148
|
+
self.states << state
|
|
149
|
+
end
|
|
150
|
+
state
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def inspect
|
|
155
|
+
"#<#{self.class} ##{__id__} states=#{state_names.inspect} events=#{event_names.inspect} options=#{options.inspect}>"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
module StateFu
|
|
2
|
+
class MethodFactory
|
|
3
|
+
|
|
4
|
+
def initialize( _binding )
|
|
5
|
+
@binding = _binding
|
|
6
|
+
define_event_methods_on( _binding )
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def install!
|
|
10
|
+
define_event_methods_on( @binding.object )
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# ensure the methods are available before calling state_fu
|
|
14
|
+
def self.prepare_class( klass )
|
|
15
|
+
return if ( klass.instance_methods + klass.private_methods + klass.protected_methods ).map(&:to_sym).include?( :method_missing_before_state_fu )
|
|
16
|
+
klass.class_eval do
|
|
17
|
+
alias_method :method_missing_before_state_fu, :method_missing
|
|
18
|
+
|
|
19
|
+
def method_missing( method_name, *args, &block )
|
|
20
|
+
if @state_fu_initialized
|
|
21
|
+
method_missing_before_state_fu( method_name, *args, &block )
|
|
22
|
+
else
|
|
23
|
+
state_fu!
|
|
24
|
+
if respond_to?( method_name )
|
|
25
|
+
send( method_name, *args, &block )
|
|
26
|
+
else
|
|
27
|
+
method_missing_before_state_fu( method_name, *args, &block )
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end # method_missing
|
|
31
|
+
end # class_eval
|
|
32
|
+
end # prepare_class
|
|
33
|
+
|
|
34
|
+
def define_method_on_metaclass( object, method_name, &block )
|
|
35
|
+
return false if object.respond_to?( method_name )
|
|
36
|
+
metaclass = class << object; self; end
|
|
37
|
+
metaclass.class_eval do
|
|
38
|
+
define_method( method_name, &block )
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def define_event_methods_on( obj )
|
|
43
|
+
_binding = @binding
|
|
44
|
+
simple, complex = @binding.machine.events.partition(&:simple? )
|
|
45
|
+
|
|
46
|
+
# method definitions for simple events (only one possible target)
|
|
47
|
+
simple.each do |event|
|
|
48
|
+
# obj.event_name( *args )
|
|
49
|
+
# returns a new transition
|
|
50
|
+
method_name = event.name
|
|
51
|
+
define_method_on_metaclass( obj, method_name ) do |*args|
|
|
52
|
+
_binding.transition( event, *args )
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# obj.event_name?()
|
|
56
|
+
# true if the event is fireable? (ie, requirements met)
|
|
57
|
+
method_name = "#{event.name}?"
|
|
58
|
+
define_method_on_metaclass( obj, method_name ) do
|
|
59
|
+
_binding.fireable?( event )
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# obj.event_name!( *args )
|
|
63
|
+
# creates, fires and returns a transition
|
|
64
|
+
method_name = "#{event.name}!"
|
|
65
|
+
define_method_on_metaclass( obj, method_name ) do |*args|
|
|
66
|
+
_binding.fire!( event, *args )
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# method definitions for complex events (target must be specified)
|
|
71
|
+
complex.each do |event|
|
|
72
|
+
# obj.event_name( target, *args )
|
|
73
|
+
# returns a new transition
|
|
74
|
+
define_method_on_metaclass( obj, event.name ) do |target, *args|
|
|
75
|
+
_binding.transition( [event, target], *args )
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# obj.event_name?( target )
|
|
79
|
+
# true if the event is fireable? (ie, requirements met)
|
|
80
|
+
method_name = "#{event.name}?"
|
|
81
|
+
define_method_on_metaclass( obj, method_name ) do |target|
|
|
82
|
+
_binding.fireable?( [event, target] )
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# obj.event_name!( target, *args )
|
|
86
|
+
# creates, fires and returns a transition
|
|
87
|
+
method_name = "#{event.name}!"
|
|
88
|
+
define_method_on_metaclass( obj, method_name ) do |target,*args|
|
|
89
|
+
_binding.fire!( [event, target], *args )
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module StateFu
|
|
2
|
+
module Persistence
|
|
3
|
+
class ActiveRecord < StateFu::Persistence::Base
|
|
4
|
+
|
|
5
|
+
def self.prepare_field( klass, field_name )
|
|
6
|
+
_field_name = field_name
|
|
7
|
+
klass.send :before_save, :state_fu!
|
|
8
|
+
# validates_presence_of _field_name
|
|
9
|
+
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# We already checked that they exist, or we'd be using the
|
|
15
|
+
# Attribute version, so just do the simplest thing we can.
|
|
16
|
+
|
|
17
|
+
def read_attribute
|
|
18
|
+
object.send( :read_attribute, field_name )
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def write_attribute( string_value )
|
|
22
|
+
object.send( :write_attribute, field_name, string_value )
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module StateFu
|
|
2
|
+
module Persistence
|
|
3
|
+
class Attribute < StateFu::Persistence::Base
|
|
4
|
+
|
|
5
|
+
def self.prepare_field( klass, field_name )
|
|
6
|
+
# ensure getter exists
|
|
7
|
+
unless klass.instance_methods.include?( field_name )
|
|
8
|
+
Logger.info "Adding attr_reader :#{field_name} for #{klass}"
|
|
9
|
+
_field_name = field_name
|
|
10
|
+
klass.class_eval do
|
|
11
|
+
private
|
|
12
|
+
attr_reader _field_name
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# ensure setter exists
|
|
17
|
+
unless klass.instance_methods.include?( "#{field_name}=" )
|
|
18
|
+
Logger.info "Adding attr_writer :#{field_name}= for #{klass}"
|
|
19
|
+
_field_name = field_name
|
|
20
|
+
klass.class_eval do
|
|
21
|
+
private
|
|
22
|
+
attr_writer _field_name
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
# Read / write our strings to a plain old instance variable
|
|
30
|
+
# Define it if it doesn't exist the first time we go to read it
|
|
31
|
+
|
|
32
|
+
def read_attribute
|
|
33
|
+
string = object.send( field_name )
|
|
34
|
+
Logger.info "Read attribute #{field_name}, got #{string} for #{object}"
|
|
35
|
+
string
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def write_attribute( string_value )
|
|
39
|
+
writer_method = "#{field_name}="
|
|
40
|
+
Logger.info "Writing attribute #{field_name} -> #{string_value} for #{object}"
|
|
41
|
+
object.send( writer_method, string_value )
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
module StateFu
|
|
2
|
+
|
|
3
|
+
class InvalidStateName < Exception
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
module Persistence
|
|
7
|
+
class Base
|
|
8
|
+
|
|
9
|
+
attr_reader :binding, :field_name, :current_state
|
|
10
|
+
|
|
11
|
+
def self.prepare_class( klass )
|
|
12
|
+
unless klass.instance_methods.include?( :method_missing_before_state_fu )
|
|
13
|
+
alias_method :method_missing_before_state_fu, :method_missing
|
|
14
|
+
klass.class_eval do
|
|
15
|
+
def method_missing( method_name, *args, &block )
|
|
16
|
+
state_fu!
|
|
17
|
+
begin
|
|
18
|
+
send( method_name, *args, &block )
|
|
19
|
+
rescue NoMethodError => e
|
|
20
|
+
method_missing_before_state_fu( method_name, *args, &block )
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.prepare_field( klass, field_name )
|
|
28
|
+
raise NotImplementedError # abstract method
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize( binding, field_name )
|
|
32
|
+
@binding = binding
|
|
33
|
+
@field_name = field_name
|
|
34
|
+
@current_state = find_current_state()
|
|
35
|
+
|
|
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?
|
|
39
|
+
else
|
|
40
|
+
persist!
|
|
41
|
+
Logger.debug("Object resumes at #{current_state.name}: #{object}")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def find_current_state
|
|
46
|
+
string = read_attribute()
|
|
47
|
+
if string.blank?
|
|
48
|
+
machine.initial_state
|
|
49
|
+
else
|
|
50
|
+
state_name = string.to_sym
|
|
51
|
+
state = machine.states[ state_name ] || raise( StateFu::InvalidStateName, string )
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def machine
|
|
56
|
+
binding.machine
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def object
|
|
60
|
+
binding.object
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def klass
|
|
64
|
+
object.class
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# def method_name
|
|
68
|
+
# binding.method_name
|
|
69
|
+
# end
|
|
70
|
+
|
|
71
|
+
def current_state=( state )
|
|
72
|
+
raise(ArgumentError, state.inspect) unless state.is_a?(StateFu::State)
|
|
73
|
+
@current_state = state
|
|
74
|
+
persist!
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def value()
|
|
78
|
+
@current_state && @current_state.name.to_s
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def persist!
|
|
82
|
+
write_attribute( value() )
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def read_attribute
|
|
88
|
+
raise "Abstract method! override me"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def write_attribute( string_value )
|
|
92
|
+
raise "Abstract method! override me"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|