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
data/lib/state_fu/logger.rb
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
+
|
|
1
2
|
require 'logger'
|
|
2
3
|
module StateFu
|
|
4
|
+
#
|
|
5
|
+
# TODO - spec coverage
|
|
6
|
+
#
|
|
7
|
+
# Provide logging facilities, including the ability to use a shared logger.
|
|
8
|
+
# Use Rails' log if running as a rails plugin; allow independent control of
|
|
9
|
+
# StateFu log level.
|
|
10
|
+
|
|
3
11
|
class Logger
|
|
4
12
|
cattr_accessor :prefix # prefix for log messages
|
|
5
13
|
cattr_accessor :suppress # set true to send messages to /dev/null
|
|
6
|
-
|
|
14
|
+
cattr_accessor :shared
|
|
15
|
+
cattr_accessor :suppress
|
|
16
|
+
|
|
7
17
|
DEBUG = 0
|
|
8
18
|
INFO = 1
|
|
9
19
|
WARN = 2
|
|
@@ -14,75 +24,142 @@ module StateFu
|
|
|
14
24
|
ENV_LOG_LEVEL = 'STATEFU_LOGLEVEL'
|
|
15
25
|
DEFAULT_LEVEL = INFO
|
|
16
26
|
|
|
17
|
-
|
|
18
|
-
SHARED_LOG_PREFIX = 'StateFu: '
|
|
27
|
+
DEFAULT_SHARED_LOG_PREFIX = '[StateFu] '
|
|
19
28
|
|
|
20
|
-
@@prefix
|
|
21
|
-
@@logger
|
|
22
|
-
@@suppress
|
|
29
|
+
@@prefix = DEFAULT_SHARED_LOG_PREFIX
|
|
30
|
+
@@logger = nil
|
|
31
|
+
@@suppress = false
|
|
32
|
+
@@shared = false
|
|
33
|
+
@@log_level = nil
|
|
23
34
|
|
|
24
|
-
def self.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def self.state_fu_log_level
|
|
36
|
-
if ENV[ ENV_LOG_LEVEL ]
|
|
37
|
-
const_get( ENV[ ENV_LOG_LEVEL ] )
|
|
35
|
+
def self.new( logger = $stdout, options={} )
|
|
36
|
+
self.suppress = false
|
|
37
|
+
self.logger = logger, options
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.initial_log_level
|
|
42
|
+
if env_level = ENV[ENV_LOG_LEVEL]
|
|
43
|
+
parse_log_level( env_level )
|
|
38
44
|
else
|
|
39
45
|
DEFAULT_LEVEL
|
|
40
46
|
end
|
|
41
47
|
end
|
|
42
48
|
|
|
43
|
-
def self.
|
|
44
|
-
|
|
49
|
+
def self.level
|
|
50
|
+
@@log_level ||= initial_log_level
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.level=( new_level )
|
|
54
|
+
@@log_level = parse_log_level(new_level)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.shared?
|
|
58
|
+
!! @@shared
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.prefix
|
|
62
|
+
shared? ? @@prefix : nil
|
|
45
63
|
end
|
|
46
64
|
|
|
47
|
-
def self.
|
|
48
|
-
|
|
65
|
+
def self.logger= logger
|
|
66
|
+
set_logger logger
|
|
49
67
|
end
|
|
50
68
|
|
|
51
69
|
def self.instance
|
|
52
|
-
@@logger ||=
|
|
70
|
+
@@logger ||= set_logger(Logger.default_logger)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.suppress!
|
|
74
|
+
self.suppress = true
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.suppressed?(severity = DEBUG)
|
|
78
|
+
suppress == true || severity < level
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.add(severity, message = nil, progname = nil, &block)
|
|
82
|
+
severity = parse_log_level( severity )
|
|
83
|
+
return if suppressed?( severity )
|
|
84
|
+
message = [prefix, (message || (block && block.call) || progname).to_s].compact.join
|
|
85
|
+
# message = "#{message}\n" unless message[-1] == ?\n
|
|
86
|
+
instance.add( severity, message )
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.debug progname = nil, █ add DEBUG, progname, █ end
|
|
90
|
+
def self.info progname = nil, █ add INFO, progname, █ end
|
|
91
|
+
def self.warn progname = nil, █ add WARN, progname, █ end
|
|
92
|
+
def self.error progname = nil, █ add ERROR, progname, █ end
|
|
93
|
+
def self.fatal progname = nil, █ add FATAL, progname, █ end
|
|
94
|
+
def self.unknown progname = nil, █ add UNKNOWN, progname, █ end
|
|
95
|
+
|
|
96
|
+
#
|
|
97
|
+
# TODO fix these crappy methods
|
|
98
|
+
#
|
|
99
|
+
|
|
100
|
+
# setter for logger instance
|
|
101
|
+
def self.set_logger( logger, options = { :shared => false } )
|
|
102
|
+
case logger
|
|
103
|
+
when String
|
|
104
|
+
file = File.open(logger, File::WRONLY | File::APPEND)
|
|
105
|
+
@@logger = Logger.activesupport_logger_available? ? ActiveSupport::BufferedLogger.new(file) : ::Logger.new(file)
|
|
106
|
+
when ::Logger
|
|
107
|
+
@@logger = logger
|
|
108
|
+
when Logger.activesupport_logger_available? && ActiveSupport::BufferedLogger
|
|
109
|
+
@@logger = logger
|
|
110
|
+
else
|
|
111
|
+
raise ArgumentError.new
|
|
112
|
+
end
|
|
113
|
+
self.shared = !!options.symbolize_keys![:shared]
|
|
114
|
+
if shared?
|
|
115
|
+
@@prefix = options[:prefix] || DEFAULT_SHARED_LOG_PREFIX
|
|
116
|
+
puts "shared :: #{@@prefix} #{prefix}"
|
|
117
|
+
end
|
|
118
|
+
if lvl = options[:level] || options[:log_level]
|
|
119
|
+
self.level = lvl
|
|
120
|
+
end
|
|
121
|
+
instance
|
|
53
122
|
end
|
|
54
123
|
|
|
55
|
-
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def self.get_logger( logr = $stdout )
|
|
56
127
|
if Object.const_defined?( "RAILS_DEFAULT_LOGGER" )
|
|
57
|
-
|
|
58
|
-
prefix = SHARED_LOG_PREFIX
|
|
128
|
+
set_logger RAILS_DEFAULT_LOGGER, :shared => true
|
|
59
129
|
else
|
|
60
130
|
if Object.const_defined?( 'ActiveSupport' ) && ActiveSupport.const_defined?('BufferedLogger')
|
|
61
|
-
|
|
131
|
+
set_logger( ActiveSupport::BufferedLogger.new( logr ))
|
|
62
132
|
else
|
|
63
|
-
|
|
64
|
-
logger.level = state_fu_log_level()
|
|
133
|
+
set_logger ::Logger.new( logr )
|
|
65
134
|
end
|
|
66
135
|
end
|
|
67
|
-
logger
|
|
68
136
|
end
|
|
69
137
|
|
|
70
|
-
def self.
|
|
71
|
-
|
|
138
|
+
def self.activesupport_logger_available?
|
|
139
|
+
Object.const_defined?( 'ActiveSupport' ) && ActiveSupport.const_defined?('BufferedLogger')
|
|
72
140
|
end
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
args[0].is_a?(String) && @@prefix
|
|
80
|
-
args[0] = @@prefix + args[0]
|
|
141
|
+
|
|
142
|
+
def self.default_logger
|
|
143
|
+
if Object.const_defined?( "RAILS_DEFAULT_LOGGER" )
|
|
144
|
+
RAILS_DEFAULT_LOGGER
|
|
145
|
+
else
|
|
146
|
+
$stdout
|
|
81
147
|
end
|
|
82
|
-
instance.send( method_id, *args )
|
|
83
148
|
end
|
|
84
|
-
|
|
149
|
+
|
|
150
|
+
def self.parse_log_level(input)
|
|
151
|
+
case input
|
|
152
|
+
when String, Symbol
|
|
153
|
+
const_get( input )
|
|
154
|
+
when 0,1,2,3,4,5
|
|
155
|
+
input
|
|
156
|
+
when nil
|
|
157
|
+
level
|
|
158
|
+
else
|
|
159
|
+
raise ArgumentError
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
85
163
|
end
|
|
86
164
|
end
|
|
87
165
|
|
|
88
|
-
# StateFu::Logger.info( StateFu::Logger.instance.inspect )
|
data/lib/state_fu/machine.rb
CHANGED
|
@@ -1,36 +1,74 @@
|
|
|
1
1
|
module StateFu
|
|
2
2
|
class Machine
|
|
3
|
-
include Helper
|
|
4
3
|
|
|
5
|
-
|
|
4
|
+
def self.BINDINGS
|
|
5
|
+
@@_bindings ||= {}
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
include Applicable
|
|
9
|
+
include HasOptions
|
|
10
|
+
|
|
11
|
+
attr_reader :hooks
|
|
12
|
+
|
|
13
|
+
#
|
|
14
|
+
# Class methods
|
|
15
|
+
#
|
|
16
|
+
|
|
6
17
|
def self.for_class(klass, name, options={}, &block)
|
|
7
18
|
options.symbolize_keys!
|
|
8
19
|
name = name.to_sym
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
machine
|
|
20
|
+
|
|
21
|
+
unless machine = klass.state_fu_machines[ name ]
|
|
22
|
+
machine = new(options)
|
|
23
|
+
machine.bind! klass, name, options[:field_name]
|
|
12
24
|
end
|
|
13
25
|
if block_given?
|
|
14
|
-
machine.apply!
|
|
26
|
+
machine.apply! &block
|
|
15
27
|
end
|
|
16
28
|
machine
|
|
17
29
|
end
|
|
18
30
|
|
|
31
|
+
# make it so that a class which has included StateFu has a binding to
|
|
32
|
+
# this machine
|
|
33
|
+
def self.bind!( machine, owner, name, field_name)
|
|
34
|
+
name = name.to_sym
|
|
35
|
+
# define an accessor method with the given name
|
|
36
|
+
if owner.class == Class
|
|
37
|
+
owner.state_fu_machines[name] = machine
|
|
38
|
+
owner.state_fu_field_names[name] = field_name
|
|
39
|
+
# method_missing to catch NoMethodError for event methods, etc
|
|
40
|
+
StateFu::MethodFactory.define_once_only_method_missing( owner )
|
|
41
|
+
unless owner.respond_to?(name)
|
|
42
|
+
owner.class_eval do
|
|
43
|
+
define_method name do
|
|
44
|
+
state_fu( name )
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
# prepare the persistence field
|
|
49
|
+
StateFu::Persistence.prepare_field owner, field_name
|
|
50
|
+
else
|
|
51
|
+
_binding = StateFu::Binding.new machine, owner, name, :field_name => field_name, :singleton => true
|
|
52
|
+
MethodFactory.define_singleton_method(owner, name) { _binding }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
19
56
|
##
|
|
20
57
|
## Instance Methods
|
|
21
58
|
##
|
|
22
59
|
|
|
23
60
|
attr_reader :states, :events, :options, :helpers, :named_procs, :requirement_messages, :tools
|
|
24
61
|
|
|
25
|
-
def initialize(
|
|
26
|
-
|
|
27
|
-
@
|
|
28
|
-
@
|
|
29
|
-
@
|
|
30
|
-
@tools = [].extend( ToolArray )
|
|
62
|
+
def initialize( options={}, &block )
|
|
63
|
+
@states = [].extend( StateArray )
|
|
64
|
+
@events = [].extend( EventArray )
|
|
65
|
+
@helpers = [].extend( HelperArray )
|
|
66
|
+
@tools = [].extend( ToolArray )
|
|
31
67
|
@named_procs = {}
|
|
32
68
|
@requirement_messages = {}
|
|
33
69
|
@options = options
|
|
70
|
+
@hooks = Hooks.for( self )
|
|
71
|
+
apply!( &block ) if block_given?
|
|
34
72
|
end
|
|
35
73
|
|
|
36
74
|
# merge the commands in &block with the existing machine; returns
|
|
@@ -52,6 +90,10 @@ module StateFu
|
|
|
52
90
|
tools.inject_into( obj )
|
|
53
91
|
end
|
|
54
92
|
|
|
93
|
+
def inject_methods_into( obj )
|
|
94
|
+
#puts 'inject_methods_into'
|
|
95
|
+
end
|
|
96
|
+
|
|
55
97
|
# the modules listed here will be mixed into Binding and
|
|
56
98
|
# Transition objects for this machine. use this to define methods,
|
|
57
99
|
# references or data useful to you during transitions, event
|
|
@@ -66,32 +108,17 @@ module StateFu
|
|
|
66
108
|
modules_to_add.each { |mod| helpers << mod }
|
|
67
109
|
end
|
|
68
110
|
|
|
69
|
-
# same
|
|
111
|
+
# same as helper, but for extending Lathes rather than the Bindings / Transitions.
|
|
112
|
+
# use this to extend the Lathe DSL to suit your problem domain.
|
|
70
113
|
def tool *modules_to_add
|
|
71
114
|
modules_to_add.each { |mod| tools << mod }
|
|
72
115
|
end
|
|
73
116
|
|
|
74
117
|
# make it so a class which has included StateFu has a binding to
|
|
75
118
|
# this machine
|
|
76
|
-
def bind!(
|
|
77
|
-
field_name ||=
|
|
78
|
-
|
|
79
|
-
StateFu::FuSpace.insert!( klass, self, name, field_name )
|
|
80
|
-
# define an accessor method with the given name
|
|
81
|
-
unless name == StateFu::DEFAULT_MACHINE
|
|
82
|
-
klass.class_eval do
|
|
83
|
-
define_method name do
|
|
84
|
-
state_fu( name )
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# method_missing to catch NoMethodError for event methods, etc
|
|
90
|
-
StateFu::MethodFactory.prepare_class( klass )
|
|
91
|
-
|
|
92
|
-
# prepare the persistence field
|
|
93
|
-
StateFu::Persistence.prepare_field( klass, field_name )
|
|
94
|
-
true
|
|
119
|
+
def bind!( owner, name= DEFAULT, field_name = nil )
|
|
120
|
+
field_name ||= Persistence.default_field_name( name )
|
|
121
|
+
self.class.bind!(self, owner, name, field_name)
|
|
95
122
|
end
|
|
96
123
|
|
|
97
124
|
def empty?
|
|
@@ -152,5 +179,6 @@ module StateFu
|
|
|
152
179
|
def graphviz
|
|
153
180
|
@graphviz ||= Plotter.new(self).output
|
|
154
181
|
end
|
|
182
|
+
|
|
155
183
|
end
|
|
156
184
|
end
|
|
@@ -1,95 +1,203 @@
|
|
|
1
1
|
module StateFu
|
|
2
|
+
# This class is responsible for defining methods at runtime.
|
|
3
|
+
#
|
|
4
|
+
# TODO: all events, simple or complex, should get the same method signature
|
|
5
|
+
# simple events will be called as: event_name! nil, *args
|
|
6
|
+
# complex events will be called as: event_name! :state, *args
|
|
7
|
+
|
|
2
8
|
class MethodFactory
|
|
3
9
|
|
|
10
|
+
# An instance of MethodFactory is created to define methods on a specific StateFu::Binding, and
|
|
11
|
+
# the object it is bound to.
|
|
12
|
+
#
|
|
13
|
+
# During the initializer, it will call define_event_methods_on(the binding), which installs
|
|
14
|
+
#
|
|
4
15
|
def initialize( _binding )
|
|
5
|
-
@binding
|
|
6
|
-
|
|
7
|
-
end
|
|
16
|
+
# store @binding in a local variable so it's accessible within
|
|
17
|
+
# the closures below (for define_singleton_method ).
|
|
8
18
|
|
|
9
|
-
|
|
10
|
-
|
|
19
|
+
# i.e, we're embedding a reference to @binding inside the method
|
|
20
|
+
@binding = _binding
|
|
21
|
+
@defs = {}
|
|
22
|
+
|
|
23
|
+
@binding.machine.states.each do |state|
|
|
24
|
+
@defs[:"#{state.name}?"] = lambda { _binding.current_state.name == state.name }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# method definitions for simple events (only one possible target)
|
|
28
|
+
@binding.machine.events.each do |event|
|
|
29
|
+
@defs[event.name] = lambda \
|
|
30
|
+
{|*args| _binding._event_method :get_transition, event, args.shift, *args }
|
|
31
|
+
@defs[:"can_#{event.name}?"] = lambda \
|
|
32
|
+
{|*args| _binding._event_method :query_transition, event, args.shift, *args }
|
|
33
|
+
@defs[:"#{event.name}!"] = lambda \
|
|
34
|
+
{|*args| _binding._event_method :fire_transition, event, args.shift, *args }
|
|
35
|
+
|
|
36
|
+
#if !event.targets.blank? # && event.targets.length > 1
|
|
37
|
+
event.targets.each do |target_state|
|
|
38
|
+
method_name = "#{event.name}_to_#{target_state.name}"
|
|
39
|
+
|
|
40
|
+
# object.event_name [:target], *arguments
|
|
41
|
+
#
|
|
42
|
+
# returns a new transition. Will raise an InvalidTransition if
|
|
43
|
+
# it is not given arguments which result in a valid combination
|
|
44
|
+
# of event and target state being deducted.
|
|
45
|
+
#
|
|
46
|
+
# object.event_name suffices without any arguments if the event
|
|
47
|
+
# has only one possible target, or only one valid target for
|
|
48
|
+
|
|
49
|
+
# object.event_name! [:target], *arguments
|
|
50
|
+
#
|
|
51
|
+
# as per the method above, except that it also
|
|
52
|
+
|
|
53
|
+
@defs[method_name.to_sym] = lambda \
|
|
54
|
+
{|*args| _binding._event_method :get_transition, event, target_state, *args }
|
|
55
|
+
|
|
56
|
+
# object.event_name! [:]
|
|
57
|
+
@defs[:"can_#{method_name}?"] = lambda \
|
|
58
|
+
{|*args| _binding._event_method :query_transition, event, target_state, *args }
|
|
59
|
+
|
|
60
|
+
@defs[:"#{method_name}!"] = lambda \
|
|
61
|
+
{|*args| _binding._event_method :fire_transition, event, target_state, *args }
|
|
62
|
+
|
|
63
|
+
end unless event.targets.nil?
|
|
64
|
+
end
|
|
11
65
|
end
|
|
12
66
|
|
|
13
|
-
#
|
|
67
|
+
#
|
|
68
|
+
# Class Methods
|
|
69
|
+
#
|
|
70
|
+
|
|
71
|
+
# This should be called once per class using StateFu. It aliases and redefines
|
|
72
|
+
# method_missing for the class.
|
|
73
|
+
#
|
|
74
|
+
# Note this happens when a machine is first bound to the class,
|
|
75
|
+
# not when StateFu is included.
|
|
76
|
+
|
|
14
77
|
def self.prepare_class( klass )
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
78
|
+
raise caller.inspect
|
|
79
|
+
self.define_once_only_method_missing( klass )
|
|
80
|
+
end # prepare_class
|
|
81
|
+
|
|
82
|
+
# When triggered, method_missing will first call state_fu!,
|
|
83
|
+
# instantating all bindings & installing their attendant
|
|
84
|
+
# MethodFactories, then check if the object now responds to the
|
|
85
|
+
# missing method name; otherwise it will call the original
|
|
86
|
+
# method_missing.
|
|
87
|
+
#
|
|
88
|
+
# method_missing will then revert to its original implementation.
|
|
89
|
+
#
|
|
90
|
+
# The purpose of all this is to allow dynamically created methods
|
|
91
|
+
# to be called, without worrying about whether they have been
|
|
92
|
+
# defined yet, and without incurring the expense of loading all
|
|
93
|
+
# the object's StateFu::Bindings before they're likely to be needed.
|
|
94
|
+
#
|
|
95
|
+
# Note that if you redefine method_missing on your StateFul
|
|
96
|
+
# classes, it's best to either do it before you include StateFu,
|
|
97
|
+
# or thoroughly understand what's happening in
|
|
98
|
+
# MethodFactory#define_once_only_method_missing.
|
|
99
|
+
|
|
100
|
+
def self.define_once_only_method_missing( klass )
|
|
101
|
+
raise ArgumentError.new(klass.to_s) unless klass.is_a?(Class)
|
|
102
|
+
|
|
103
|
+
klass.class_eval do
|
|
104
|
+
return false if @_state_fu_prepared
|
|
105
|
+
@_state_fu_prepared = true
|
|
106
|
+
|
|
107
|
+
alias_method(:method_missing_before_state_fu, :method_missing) # if defined?(:method_missing, true)
|
|
108
|
+
|
|
109
|
+
def method_missing(method_name, *args, &block)
|
|
110
|
+
# invoke state_fu! to ensure event, etc methods are defined
|
|
111
|
+
begin
|
|
112
|
+
state_fu! unless defined? initialize_state_fu!
|
|
113
|
+
rescue NoMethodError => e
|
|
114
|
+
raise e
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# reset method_missing for this instance
|
|
118
|
+
class << self; self; end.class_eval do
|
|
119
|
+
alias_method :method_missing, :method_missing_before_state_fu
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# call the newly defined method, or the original method_missing
|
|
123
|
+
if respond_to?(method_name, true)
|
|
124
|
+
# it was defined by calling state_fu!, which instantiated bindings
|
|
125
|
+
# for its state machines, which defined singleton methods for its
|
|
126
|
+
# states & events when it was constructed.
|
|
127
|
+
__send__( method_name, *args, &block )
|
|
128
|
+
else
|
|
129
|
+
# call the original method_missing (method_missing_before_state_fu)
|
|
130
|
+
method_missing( method_name, *args, &block )
|
|
29
131
|
end
|
|
30
132
|
end # method_missing
|
|
31
133
|
end # class_eval
|
|
32
|
-
end #
|
|
134
|
+
end # define_once_only_method_missing
|
|
33
135
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
136
|
+
# Define the same helper methods on the StateFu::Binding and its
|
|
137
|
+
# object. Any existing methods will not be tampered with, but a
|
|
138
|
+
# warning will be issued in the logs if any methods cannot be defined.
|
|
139
|
+
def install!
|
|
140
|
+
define_event_methods_on( @binding )
|
|
141
|
+
define_event_methods_on( @binding.object )
|
|
40
142
|
end
|
|
41
143
|
|
|
144
|
+
#
|
|
145
|
+
# For each event, on the given object, define three methods.
|
|
146
|
+
# - The first method is the same as the event name.
|
|
147
|
+
# Returns a new, unfired transition object.
|
|
148
|
+
# - The second method has a "?" suffix.
|
|
149
|
+
# Returns true if the event can be fired.
|
|
150
|
+
# - The third method has a "!" suffix.
|
|
151
|
+
# Creates a new Transition, fires and returns it once complete.
|
|
152
|
+
#
|
|
153
|
+
# The arguments expected depend on whether the event is "simple" - ie,
|
|
154
|
+
# has only one possible target state.
|
|
155
|
+
#
|
|
156
|
+
# All simple event methods pass their entire argument list
|
|
157
|
+
# directly to transition. These arguments can be accessed inside
|
|
158
|
+
# event hooks, requirements, etc by calling Transition#args.
|
|
159
|
+
#
|
|
160
|
+
# All complex event methods require their first argument to be a
|
|
161
|
+
# Symbol containing a valid target State's name, or the State
|
|
162
|
+
# itself. The remaining arguments are passed into the transition,
|
|
163
|
+
# as with simple event methods.
|
|
164
|
+
#
|
|
42
165
|
def define_event_methods_on( obj )
|
|
43
|
-
|
|
44
|
-
|
|
166
|
+
@defs.each do |method_name, method_body|
|
|
167
|
+
define_singleton_method( obj, method_name, &method_body)
|
|
168
|
+
end
|
|
169
|
+
end # define_event_methods_on
|
|
45
170
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
171
|
+
def define_singleton_method( object, method_name, &block )
|
|
172
|
+
MethodFactory.define_singleton_method object, method_name, &block
|
|
173
|
+
end
|
|
54
174
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
175
|
+
# define a a method on the metaclass of the given object. The
|
|
176
|
+
# resulting "singleton method" will be unique to that instance,
|
|
177
|
+
# not shared by other instances of its class.
|
|
178
|
+
#
|
|
179
|
+
# This allows us to embed a reference to the instance's unique
|
|
180
|
+
# binding in the new method.
|
|
181
|
+
#
|
|
182
|
+
# existing methods will never be overwritten.
|
|
61
183
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
184
|
+
def self.define_singleton_method( object, method_name, options={}, &block )
|
|
185
|
+
if object.respond_to?(method_name, true)
|
|
186
|
+
msg = !options[:force]
|
|
187
|
+
Logger.info "Existing method #{method(method_name) rescue [method_name].inspect} "\
|
|
188
|
+
"for #{object.class} #{object} "\
|
|
189
|
+
"#{options[:force] ? 'WILL' : 'won\'t'} "\
|
|
190
|
+
"be overwritten."
|
|
191
|
+
else
|
|
192
|
+
metaclass = class << object; self; end
|
|
193
|
+
metaclass.class_eval do
|
|
194
|
+
define_method( method_name, &block )
|
|
67
195
|
end
|
|
68
196
|
end
|
|
197
|
+
end
|
|
198
|
+
alias_method :define_singleton_method, :define_singleton_method
|
|
69
199
|
|
|
70
|
-
|
|
71
|
-
|
|
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, *args|
|
|
82
|
-
_binding.fireable?( [event, target], *args )
|
|
83
|
-
end
|
|
200
|
+
end # class MethodFactory
|
|
201
|
+
end # module StateFu
|
|
84
202
|
|
|
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
203
|
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module StateFu
|
|
2
|
+
module Methodical
|
|
3
|
+
|
|
4
|
+
def self.__define_method( method_name, &block )
|
|
5
|
+
self.class.class_eval do
|
|
6
|
+
define_method method_name, &block
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def __define_singleton_method( method_name, &block )
|
|
11
|
+
(class << object; self; end).class_eval do
|
|
12
|
+
define_method method_name, &block
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -5,7 +5,12 @@ module StateFu
|
|
|
5
5
|
def self.prepare_field( klass, field_name )
|
|
6
6
|
_field_name = field_name
|
|
7
7
|
Logger.debug("Preparing ActiveRecord field #{klass}.#{field_name}")
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
# this adds a before_save hook to ensure that the field is initialized
|
|
10
|
+
# (and the initial state set) before create.
|
|
11
|
+
klass.send :before_create, :state_fu!
|
|
12
|
+
|
|
13
|
+
# it's usually a good idea to do this:
|
|
9
14
|
# validates_presence_of _field_name
|
|
10
15
|
end
|
|
11
16
|
|