require 'state_machine/eval_helpers' module StateMachine # Represents a type of module in which class-level methods are proxied to # another class, injecting a custom :if condition along with method. # # This is used for being able to automatically include conditionals which # check the current state in class-level methods that have configuration # options. # # == Examples # # class Vehicle # class << self # attr_accessor :validations # # def validate(options, &block) # validations << options # end # end # # self.validations = [] # attr_accessor :state, :simulate # # def moving? # self.class.validations.all? {|validation| validation[:if].call(self)} # end # end # # In the above class, a simple set of validation behaviors have been defined. # Each validation consists of a configuration like so: # # Vehicle.validate :unless => :simulate # Vehicle.validate :if => lambda {|vehicle| ...} # # In order to scope conditions, a condition proxy can be created to the # Vehicle class. For example, # # proxy = StateMachine::ConditionProxy.new(Vehicle, lambda {|vehicle| vehicle.state == 'first_gear'}) # proxy.validate(:unless => :simulate) # # vehicle = Vehicle.new # => # # vehicle.moving? # => false # # vehicle.state = 'first_gear' # vehicle.moving? # => true # # vehicle.simulate = true # vehicle.moving? # => false class ConditionProxy < Module include EvalHelpers # Creates a new proxy to the given class, merging in the given condition def initialize(klass, condition) @klass = klass @condition = condition end # Hooks in condition merging to methods that don't exist in this module def method_missing(*args, &block) # Get the configuration if args.last.is_a?(Hash) options = args.last else args << options = {} end # Get any existing condition that may need to be merged if_condition = options.delete(:if) unless_condition = options.delete(:unless) # Provide scope access to configuration in case the block is evaluated # within the object instance proxy = self proxy_condition = @condition # Replace the configuration condition with the one configured for this # proxy, merging together any existing conditions options[:if] = lambda do |*args| # Block may be executed within the context of the actual object, so it'll # either be the first argument or the executing context object = args.first || self proxy.evaluate_method(object, proxy_condition) && Array(if_condition).all? {|condition| proxy.evaluate_method(object, condition)} && !Array(unless_condition).any? {|condition| proxy.evaluate_method(object, condition)} end # Evaluate the method on the original class with the condition proxied # through @klass.send(*args, &block) end end end