davidlee-state-fu 0.3.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. data/README.textile +124 -34
  2. data/Rakefile +36 -30
  3. data/lib/no_stdout.rb +1 -1
  4. data/lib/state-fu.rb +9 -8
  5. data/lib/state_fu/active_support_lite/array/access.rb +12 -5
  6. data/lib/state_fu/active_support_lite/array/conversions.rb +10 -4
  7. data/lib/state_fu/active_support_lite/array/extract_options.rb +5 -4
  8. data/lib/state_fu/active_support_lite/array/grouping.rb +7 -4
  9. data/lib/state_fu/active_support_lite/array/random_access.rb +4 -3
  10. data/lib/state_fu/active_support_lite/array/wrapper.rb +4 -3
  11. data/lib/state_fu/active_support_lite/array.rb +3 -1
  12. data/lib/state_fu/active_support_lite/blank.rb +18 -9
  13. data/lib/state_fu/active_support_lite/cattr_reader.rb +4 -1
  14. data/lib/state_fu/active_support_lite/keys.rb +8 -3
  15. data/lib/state_fu/active_support_lite/misc.rb +6 -4
  16. data/lib/state_fu/active_support_lite/module/delegation.rb +130 -0
  17. data/lib/state_fu/active_support_lite/module.rb +1 -0
  18. data/lib/state_fu/active_support_lite/object.rb +5 -2
  19. data/lib/state_fu/active_support_lite/string.rb +6 -1
  20. data/lib/state_fu/active_support_lite/symbol.rb +2 -1
  21. data/lib/state_fu/applicable.rb +41 -0
  22. data/lib/state_fu/{helper.rb → arrays.rb} +45 -121
  23. data/lib/state_fu/binding.rb +136 -159
  24. data/lib/state_fu/core_ext.rb +78 -10
  25. data/lib/state_fu/event.rb +112 -48
  26. data/lib/state_fu/exceptions.rb +80 -34
  27. data/lib/state_fu/executioner.rb +149 -0
  28. data/lib/state_fu/has_options.rb +16 -0
  29. data/lib/state_fu/hooks.rb +21 -16
  30. data/lib/state_fu/interface.rb +80 -83
  31. data/lib/state_fu/lathe.rb +361 -148
  32. data/lib/state_fu/logger.rb +122 -45
  33. data/lib/state_fu/machine.rb +60 -32
  34. data/lib/state_fu/method_factory.rb +180 -72
  35. data/lib/state_fu/methodical.rb +17 -0
  36. data/lib/state_fu/persistence/active_record.rb +6 -1
  37. data/lib/state_fu/persistence/attribute.rb +1 -0
  38. data/lib/state_fu/persistence/base.rb +8 -6
  39. data/lib/state_fu/persistence.rb +94 -23
  40. data/lib/state_fu/sprocket.rb +26 -11
  41. data/lib/state_fu/state.rb +8 -27
  42. data/lib/state_fu/transition.rb +207 -98
  43. data/lib/state_fu/transition_query.rb +214 -0
  44. data/lib/state_fu.rb +1 -0
  45. data/lib/tasks/spec_last.rake +46 -0
  46. data/lib/tasks/state_fu.rake +57 -0
  47. data/lib/vizier.rb +61 -61
  48. data/spec/custom_formatter.rb +49 -0
  49. data/spec/features/binding_and_transition_helper_mixin_spec.rb +2 -2
  50. data/spec/features/method_missing_only_once_spec.rb +28 -0
  51. data/spec/features/not_requirements_spec.rb +83 -46
  52. data/spec/features/plotter_spec.rb +97 -0
  53. data/spec/features/shared_log_spec.rb +7 -0
  54. data/spec/features/singleton_machine_spec.rb +39 -0
  55. data/spec/features/state_and_array_options_accessor_spec.rb +1 -1
  56. data/spec/features/{transition_boolean_comparison.rb → transition_boolean_comparison_spec.rb} +29 -18
  57. data/spec/helper.rb +6 -117
  58. data/spec/integration/active_record_persistence_spec.rb +18 -4
  59. data/spec/integration/binding_extension_spec.rb +1 -1
  60. data/spec/integration/class_accessor_spec.rb +49 -59
  61. data/spec/integration/event_definition_spec.rb +20 -20
  62. data/spec/integration/example_01_document_spec.rb +13 -8
  63. data/spec/integration/example_02_string_spec.rb +3 -2
  64. data/spec/integration/instance_accessor_spec.rb +16 -19
  65. data/spec/integration/lathe_extension_spec.rb +2 -2
  66. data/spec/integration/machine_duplication_spec.rb +59 -37
  67. data/spec/integration/relaxdb_persistence_spec.rb +6 -3
  68. data/spec/integration/requirement_reflection_spec.rb +66 -57
  69. data/spec/integration/state_definition_spec.rb +72 -66
  70. data/spec/integration/transition_spec.rb +169 -173
  71. data/spec/spec.opts +5 -3
  72. data/spec/spec_helper.rb +132 -0
  73. data/spec/state_fu_spec.rb +870 -0
  74. data/spec/units/binding_spec.rb +33 -22
  75. data/spec/units/event_spec.rb +3 -22
  76. data/spec/units/exceptions_spec.rb +7 -0
  77. data/spec/units/lathe_spec.rb +7 -7
  78. data/spec/units/machine_spec.rb +67 -75
  79. data/spec/units/method_factory_spec.rb +55 -48
  80. data/spec/units/sprocket_spec.rb +5 -7
  81. data/spec/units/state_spec.rb +33 -24
  82. metadata +31 -19
  83. data/lib/state_fu/active_support_lite/inheritable_attributes.rb +0 -1
  84. data/lib/state_fu/fu_space.rb +0 -51
  85. data/lib/state_fu/mock_transition.rb +0 -38
  86. data/spec/BDD/plotter_spec.rb +0 -115
  87. data/spec/integration/dynamic_requirement_spec.rb +0 -160
  88. data/spec/integration/ex_machine_for_accounts_spec.rb +0 -79
  89. data/spec/integration/sanity_spec.rb +0 -31
  90. data/spec/units/fu_space_spec.rb +0 -95
@@ -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
- DEFAULT_PREFIX = nil
18
- SHARED_LOG_PREFIX = 'StateFu: '
27
+ DEFAULT_SHARED_LOG_PREFIX = '[StateFu] '
19
28
 
20
- @@prefix = DEFAULT_PREFIX
21
- @@logger = nil
22
- @@suppress = false
29
+ @@prefix = DEFAULT_SHARED_LOG_PREFIX
30
+ @@logger = nil
31
+ @@suppress = false
32
+ @@shared = false
33
+ @@log_level = nil
23
34
 
24
- def self.level=( new_level )
25
- instance.level = case new_level
26
- when String, Symbol
27
- const_get( new_level )
28
- when Fixnum
29
- new_level
30
- else
31
- state_fu_log_level()
32
- end
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.new( log = $stdout, level = () )
44
- self.instance = get_logger( log )
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.instance=( logger )
48
- @@logger ||= get_logger
65
+ def self.logger= logger
66
+ set_logger logger
49
67
  end
50
68
 
51
69
  def self.instance
52
- @@logger ||= get_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, &block; add DEBUG, progname, &block; end
90
+ def self.info progname = nil, &block; add INFO, progname, &block; end
91
+ def self.warn progname = nil, &block; add WARN, progname, &block; end
92
+ def self.error progname = nil, &block; add ERROR, progname, &block; end
93
+ def self.fatal progname = nil, &block; add FATAL, progname, &block; end
94
+ def self.unknown progname = nil, &block; add UNKNOWN, progname, &block; 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
- def self.get_logger( log = $stdout )
124
+ private
125
+
126
+ def self.get_logger( logr = $stdout )
56
127
  if Object.const_defined?( "RAILS_DEFAULT_LOGGER" )
57
- logger = RAILS_DEFAULT_LOGGER
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
- logger = ActiveSupport::BufferedLogger.new( log )
131
+ set_logger( ActiveSupport::BufferedLogger.new( logr ))
62
132
  else
63
- logger = ::Logger.new( log )
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.suppress!
71
- @@suppress = true
138
+ def self.activesupport_logger_available?
139
+ Object.const_defined?( 'ActiveSupport' ) && ActiveSupport.const_defined?('BufferedLogger')
72
140
  end
73
-
74
- # method_missing is usually a last resort
75
- # but i don't see it causing any headaches here.
76
- def self.method_missing( method_id, *args )
77
- return if @@suppress
78
- if [:debug, :info, :warn, :error, :fatal].include?( method_id ) &&
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 )
@@ -1,36 +1,74 @@
1
1
  module StateFu
2
2
  class Machine
3
- include Helper
4
3
 
5
- # meta-constructor; expects to be called via Klass.machine()
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
- unless machine = StateFu::FuSpace.class_machines[ klass ][ name ]
10
- machine = new( name, options, &block )
11
- machine.bind!( klass, name, options[:field_name] )
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!( &block )
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( name, options={}, &block )
26
- # TODO - name isn't actually used anywhere yet - remove from constructor
27
- @states = [].extend( StateArray )
28
- @events = [].extend( EventArray )
29
- @helpers = [].extend( HelperArray )
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 deal but for extending Lathe
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!( klass, name=StateFu::DEFAULT_MACHINE, field_name = nil )
77
- field_name ||= name.to_s.underscore.tr(' ', '_') + StateFu::Persistence::DEFAULT_FIELD_NAME_SUFFIX
78
- field_name = field_name.to_sym
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 = _binding
6
- define_event_methods_on( _binding )
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
- def install!
10
- define_event_methods_on( @binding.object )
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
- # ensure the methods are available before calling state_fu
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
- 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
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 # prepare_class
134
+ end # define_once_only_method_missing
33
135
 
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
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
- _binding = @binding
44
- simple, complex = @binding.machine.events.partition(&:simple? )
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
- # 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
171
+ def define_singleton_method( object, method_name, &block )
172
+ MethodFactory.define_singleton_method object, method_name, &block
173
+ end
54
174
 
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
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
- # 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 )
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
- # 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, *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
- klass.send :before_save, :state_fu!
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
 
@@ -24,6 +24,7 @@ module StateFu
24
24
  end
25
25
  end
26
26
 
27
+ def b; binding; end
27
28
  private
28
29
 
29
30
  # Read / write our strings to a plain old instance variable