lib/state_fu/method_factory.rb in davidlee-state-fu-0.3.1 vs lib/state_fu/method_factory.rb in davidlee-state-fu-0.10.0
- old
+ new
@@ -1,95 +1,203 @@
module StateFu
+ # This class is responsible for defining methods at runtime.
+ #
+ # TODO: all events, simple or complex, should get the same method signature
+ # simple events will be called as: event_name! nil, *args
+ # complex events will be called as: event_name! :state, *args
+
class MethodFactory
+ # An instance of MethodFactory is created to define methods on a specific StateFu::Binding, and
+ # the object it is bound to.
+ #
+ # During the initializer, it will call define_event_methods_on(the binding), which installs
+ #
def initialize( _binding )
- @binding = _binding
- define_event_methods_on( _binding )
- end
+ # store @binding in a local variable so it's accessible within
+ # the closures below (for define_singleton_method ).
- def install!
- define_event_methods_on( @binding.object )
+ # i.e, we're embedding a reference to @binding inside the method
+ @binding = _binding
+ @defs = {}
+
+ @binding.machine.states.each do |state|
+ @defs[:"#{state.name}?"] = lambda { _binding.current_state.name == state.name }
+ end
+
+ # method definitions for simple events (only one possible target)
+ @binding.machine.events.each do |event|
+ @defs[event.name] = lambda \
+ {|*args| _binding._event_method :get_transition, event, args.shift, *args }
+ @defs[:"can_#{event.name}?"] = lambda \
+ {|*args| _binding._event_method :query_transition, event, args.shift, *args }
+ @defs[:"#{event.name}!"] = lambda \
+ {|*args| _binding._event_method :fire_transition, event, args.shift, *args }
+
+ #if !event.targets.blank? # && event.targets.length > 1
+ event.targets.each do |target_state|
+ method_name = "#{event.name}_to_#{target_state.name}"
+
+ # object.event_name [:target], *arguments
+ #
+ # returns a new transition. Will raise an InvalidTransition if
+ # it is not given arguments which result in a valid combination
+ # of event and target state being deducted.
+ #
+ # object.event_name suffices without any arguments if the event
+ # has only one possible target, or only one valid target for
+
+ # object.event_name! [:target], *arguments
+ #
+ # as per the method above, except that it also
+
+ @defs[method_name.to_sym] = lambda \
+ {|*args| _binding._event_method :get_transition, event, target_state, *args }
+
+ # object.event_name! [:]
+ @defs[:"can_#{method_name}?"] = lambda \
+ {|*args| _binding._event_method :query_transition, event, target_state, *args }
+
+ @defs[:"#{method_name}!"] = lambda \
+ {|*args| _binding._event_method :fire_transition, event, target_state, *args }
+
+ end unless event.targets.nil?
+ end
end
- # ensure the methods are available before calling state_fu
+ #
+ # Class Methods
+ #
+
+ # This should be called once per class using StateFu. It aliases and redefines
+ # method_missing for the class.
+ #
+ # Note this happens when a machine is first bound to the class,
+ # not when StateFu is included.
+
def self.prepare_class( klass )
- return if ( klass.instance_methods + klass.private_methods + klass.protected_methods ).map(&:to_sym).include?( :method_missing_before_state_fu )
- klass.class_eval do
- alias_method :method_missing_before_state_fu, :method_missing
+ raise caller.inspect
+ self.define_once_only_method_missing( klass )
+ end # prepare_class
- def method_missing( method_name, *args, &block )
- if @state_fu_initialized
- method_missing_before_state_fu( method_name, *args, &block )
- else
- state_fu!
- if respond_to?( method_name )
- send( method_name, *args, &block )
- else
- method_missing_before_state_fu( method_name, *args, &block )
- end
+ # When triggered, method_missing will first call state_fu!,
+ # instantating all bindings & installing their attendant
+ # MethodFactories, then check if the object now responds to the
+ # missing method name; otherwise it will call the original
+ # method_missing.
+ #
+ # method_missing will then revert to its original implementation.
+ #
+ # The purpose of all this is to allow dynamically created methods
+ # to be called, without worrying about whether they have been
+ # defined yet, and without incurring the expense of loading all
+ # the object's StateFu::Bindings before they're likely to be needed.
+ #
+ # Note that if you redefine method_missing on your StateFul
+ # classes, it's best to either do it before you include StateFu,
+ # or thoroughly understand what's happening in
+ # MethodFactory#define_once_only_method_missing.
+
+ def self.define_once_only_method_missing( klass )
+ raise ArgumentError.new(klass.to_s) unless klass.is_a?(Class)
+
+ klass.class_eval do
+ return false if @_state_fu_prepared
+ @_state_fu_prepared = true
+
+ alias_method(:method_missing_before_state_fu, :method_missing) # if defined?(:method_missing, true)
+
+ def method_missing(method_name, *args, &block)
+ # invoke state_fu! to ensure event, etc methods are defined
+ begin
+ state_fu! unless defined? initialize_state_fu!
+ rescue NoMethodError => e
+ raise e
end
+
+ # reset method_missing for this instance
+ class << self; self; end.class_eval do
+ alias_method :method_missing, :method_missing_before_state_fu
+ end
+
+ # call the newly defined method, or the original method_missing
+ if respond_to?(method_name, true)
+ # it was defined by calling state_fu!, which instantiated bindings
+ # for its state machines, which defined singleton methods for its
+ # states & events when it was constructed.
+ __send__( method_name, *args, &block )
+ else
+ # call the original method_missing (method_missing_before_state_fu)
+ method_missing( method_name, *args, &block )
+ end
end # method_missing
end # class_eval
- end # prepare_class
+ end # define_once_only_method_missing
- def define_method_on_metaclass( object, method_name, &block )
- return false if object.respond_to?( method_name )
- metaclass = class << object; self; end
- metaclass.class_eval do
- define_method( method_name, &block )
- end
+ # Define the same helper methods on the StateFu::Binding and its
+ # object. Any existing methods will not be tampered with, but a
+ # warning will be issued in the logs if any methods cannot be defined.
+ def install!
+ define_event_methods_on( @binding )
+ define_event_methods_on( @binding.object )
end
+ #
+ # For each event, on the given object, define three methods.
+ # - The first method is the same as the event name.
+ # Returns a new, unfired transition object.
+ # - The second method has a "?" suffix.
+ # Returns true if the event can be fired.
+ # - The third method has a "!" suffix.
+ # Creates a new Transition, fires and returns it once complete.
+ #
+ # The arguments expected depend on whether the event is "simple" - ie,
+ # has only one possible target state.
+ #
+ # All simple event methods pass their entire argument list
+ # directly to transition. These arguments can be accessed inside
+ # event hooks, requirements, etc by calling Transition#args.
+ #
+ # All complex event methods require their first argument to be a
+ # Symbol containing a valid target State's name, or the State
+ # itself. The remaining arguments are passed into the transition,
+ # as with simple event methods.
+ #
def define_event_methods_on( obj )
- _binding = @binding
- simple, complex = @binding.machine.events.partition(&:simple? )
+ @defs.each do |method_name, method_body|
+ define_singleton_method( obj, method_name, &method_body)
+ end
+ end # define_event_methods_on
- # method definitions for simple events (only one possible target)
- simple.each do |event|
- # obj.event_name( *args )
- # returns a new transition
- method_name = event.name
- define_method_on_metaclass( obj, method_name ) do |*args|
- _binding.transition( event, *args )
- end
+ def define_singleton_method( object, method_name, &block )
+ MethodFactory.define_singleton_method object, method_name, &block
+ end
- # obj.event_name?()
- # true if the event is fireable? (ie, requirements met)
- method_name = "#{event.name}?"
- define_method_on_metaclass( obj, method_name ) do
- _binding.fireable?( event )
- end
+ # define a a method on the metaclass of the given object. The
+ # resulting "singleton method" will be unique to that instance,
+ # not shared by other instances of its class.
+ #
+ # This allows us to embed a reference to the instance's unique
+ # binding in the new method.
+ #
+ # existing methods will never be overwritten.
- # obj.event_name!( *args )
- # creates, fires and returns a transition
- method_name = "#{event.name}!"
- define_method_on_metaclass( obj, method_name ) do |*args|
- _binding.fire!( event, *args )
+ def self.define_singleton_method( object, method_name, options={}, &block )
+ if object.respond_to?(method_name, true)
+ msg = !options[:force]
+ Logger.info "Existing method #{method(method_name) rescue [method_name].inspect} "\
+ "for #{object.class} #{object} "\
+ "#{options[:force] ? 'WILL' : 'won\'t'} "\
+ "be overwritten."
+ else
+ metaclass = class << object; self; end
+ metaclass.class_eval do
+ define_method( method_name, &block )
end
end
+ end
+ alias_method :define_singleton_method, :define_singleton_method
- # method definitions for complex events (target must be specified)
- complex.each do |event|
- # obj.event_name( target, *args )
- # returns a new transition
- define_method_on_metaclass( obj, event.name ) do |target, *args|
- _binding.transition( [event, target], *args )
- end
+ end # class MethodFactory
+end # module StateFu
- # obj.event_name?( target )
- # true if the event is fireable? (ie, requirements met)
- method_name = "#{event.name}?"
- define_method_on_metaclass( obj, method_name ) do |target, *args|
- _binding.fireable?( [event, target], *args )
- end
- # obj.event_name!( target, *args )
- # creates, fires and returns a transition
- method_name = "#{event.name}!"
- define_method_on_metaclass( obj, method_name ) do |target, *args|
- _binding.fire!( [event, target], *args )
- end
-
- end
- end
- end
-end