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