Module: StateMachines::EvalHelpers

Included in:
Branch, Callback, Machine, StateContext
Defined in:
lib/state_machines/eval_helpers.rb

Overview

Provides a set of helper methods for evaluating methods within the context of an object.

Instance Method Summary collapse

Instance Method Details

#evaluate_method(object, method, *args, &block) ⇒ Object

Evaluates one of several different types of methods within the context of the given object. Methods can be one of the following types:

  • Symbol

  • Method / Proc

  • String

Examples

Below are examples of the various ways that a method can be evaluated on an object:

class Person
  def initialize(name)
    @name = name
  end

  def name
    @name
  end
end

class PersonCallback
  def self.run(person)
    person.name
  end
end

person = Person.new('John Smith')

evaluate_method(person, :name)                            # => "John Smith"
evaluate_method(person, PersonCallback.method(:run))      # => "John Smith"
evaluate_method(person, Proc.new {|person| person.name})  # => "John Smith"
evaluate_method(person, lambda {|person| person.name})    # => "John Smith"
evaluate_method(person, '@name')                          # => "John Smith"

Additional arguments

Additional arguments can be passed to the methods being evaluated. If the method defines additional arguments other than the object context, then all arguments are required.

For guard conditions in state machines, event arguments can be passed automatically based on the guard’s arity:

  • Guards with arity 1 receive only the object (backward compatible)

  • Guards with arity -1 or > 1 receive object + event arguments

For example,

person = Person.new('John Smith')

evaluate_method(person, lambda {|person| person.name}, 21)                              # => "John Smith"
evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21)          # => "John Smith is 21"
evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21, 'male')  # => ArgumentError: wrong number of arguments (3 for 2)

With event arguments for guards:

# Single parameter guard (backward compatible)
guard = lambda {|obj| obj.valid? }
evaluate_method_with_event_args(object, guard, [arg1, arg2])  # => calls guard.call(object)

# Multi-parameter guard (receives event args)
guard = lambda {|obj, *args| obj.valid? && args[0] == :force }
evaluate_method_with_event_args(object, guard, [:force])      # => calls guard.call(object, :force)


72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/state_machines/eval_helpers.rb', line 72

def evaluate_method(object, method, *args, **, &block)
  case method
  in Symbol => sym
    klass = (class << object; self; end)
    args = [] if (klass.method_defined?(sym) || klass.private_method_defined?(sym)) && object.method(sym).arity.zero?
    object.send(sym, *args, **, &block)
  in Proc => proc
    args.unshift(object)
    arity = proc.arity
    # Handle blocks for Procs
    case [block_given?, arity]
    in [true, arity] if arity != 0
      case arity
      in 1 | 2
        # Force the block to be either the only argument or the second one
        # after the object (may mean additional arguments get discarded)
        args = args[0, arity - 1] + [block]
      else
        # insert the block to the end of the args
        args << block
      end
    in [_, 0 | 1]
      # These method types are only called with 0, 1, or n arguments
      args = args[0, arity]
    else
      # No changes needed for other cases
    end

    # Call the Proc with the arguments
    proc.call(*args, **)

  in Method => meth
    args.unshift(object)
    arity = meth.arity

    # Methods handle blocks via &block, not as arguments
    # Only limit arguments if necessary based on arity
    args = args[0, arity] if [0, 1].include?(arity)

    # Call the Method with the arguments and pass the block
    meth.call(*args, **, &block)
  in String => str
    # Input validation for string evaluation
    validate_eval_string(str)

    case [block_given?, StateMachines::Transition.pause_supported?]
    in [true, true]
      eval(str, object.instance_eval { binding }, &block)
    in [true, false]
      # Support for JRuby and Truffle Ruby, which don't support binding blocks
      # Need to check with @headius, if jruby 10 does now.
      eigen = class << object; self; end
      eigen.class_eval "            def __temp_eval_method__(*args, &b)\n              \#{str}\n            end\n      RUBY\n      result = object.__temp_eval_method__(*args, &block)\n      eigen.send(:remove_method, :__temp_eval_method__)\n      result\n    in [false, _]\n      eval(str, object.instance_eval { binding })\n    end\n  else\n    raise ArgumentError, 'Methods must be a symbol denoting the method to call, a block to be invoked, or a string to be evaluated'\n  end\nend\n", __FILE__, __LINE__ + 1

#evaluate_method_with_event_args(object, method, event_args = []) ⇒ Object

Evaluates a guard method with support for event arguments passed to transitions. This method uses arity detection to determine whether to pass event arguments to the guard, ensuring backward compatibility.

Parameters

  • object - The object context to evaluate within

  • method - The guard method/proc to evaluate

  • event_args - Array of arguments passed to the event (optional)

Arity-based behavior

  • Arity 1: Only passes the object (backward compatible)

  • Arity -1 or > 1: Passes object + event arguments

Examples

# Backward compatible single-parameter guard
guard = lambda {|obj| obj.valid? }
evaluate_method_with_event_args(object, guard, [:force])  # => calls guard.call(object)

# New multi-parameter guard receiving event args
guard = lambda {|obj, *args| obj.valid? && args[0] != :skip }
evaluate_method_with_event_args(object, guard, [:skip])   # => calls guard.call(object, :skip)


162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/state_machines/eval_helpers.rb', line 162

def evaluate_method_with_event_args(object, method, event_args = [])
  case method
  in Symbol
    # Symbol methods currently don't support event arguments
    # This maintains backward compatibility
    evaluate_method(object, method)
  in Proc => proc
    arity = proc.arity

    # Arity-based decision for backward compatibility using pattern matching
    case arity
    in 0
      proc.call
    in 1
      proc.call(object)
    in -1
      # Splat parameters: object + all event args
      proc.call(object, *event_args)
    in arity if arity > 1
      # Explicit parameters: object + limited event args
      args_needed = arity - 1 # Subtract 1 for the object parameter
      proc.call(object, *event_args[0, args_needed])
    else
      # Negative arity other than -1 (unlikely but handle gracefully)
      proc.call(object, *event_args)
    end
  in Method => meth
    arity = meth.arity

    case arity
    in 0
      meth.call
    in 1
      meth.call(object)
    in -1
      meth.call(object, *event_args)
    in arity if arity > 1
      args_needed = arity - 1
      meth.call(object, *event_args[0, args_needed])
    else
      meth.call(object, *event_args)
    end
  in String
    # String evaluation doesn't support event arguments for security
    evaluate_method(object, method)
  else
    # Fall back to standard evaluation
    evaluate_method(object, method)
  end
end