From 2d3fdbf5a43de9b366508a4cbba49382f23c0f68 Mon Sep 17 00:00:00 2001 From: Yousuf J Date: Wed, 6 Aug 2025 18:10:00 -0400 Subject: [PATCH 1/2] Update conditional argument handling to correctly support lambdas --- lib/workflow/event.rb | 10 +++++++--- test/conditionals_test.rb | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/workflow/event.rb b/lib/workflow/event.rb index 341c7e5..c425dd7 100644 --- a/lib/workflow/event.rb +++ b/lib/workflow/event.rb @@ -28,9 +28,13 @@ def condition_applicable?(object, event_arguments) object.send(condition, *event_arguments) end else - # since blocks can ignore extra arguments without raising an error in Ruby, - # no `if` is needed - compare with `arity` switch in above methods handling - condition.call(object, *event_arguments) + # Blocks and non-lambda Procs can ignore extra arguments without raising an error in Ruby, + # but lambdas cannot, so we still have to check arity + if condition.arity == 1 # no additional parameters accepted + condition.call(object) + else + condition.call(object, *event_arguments) + end end else true diff --git a/test/conditionals_test.rb b/test/conditionals_test.rb index f4352bb..6f9fae3 100644 --- a/test/conditionals_test.rb +++ b/test/conditionals_test.rb @@ -92,7 +92,8 @@ def check_low_battery?() # supports no arguments, lets test below, what happens event :turn_on, :transitions_to => :low_battery # otherwise end state :on do - event :check, :transitions_to => :low_battery, :if => proc { |obj| return false } + # Use a lambda proc, which enforces correct arity + event :check, :transitions_to => :low_battery, :if => -> (obj) { return false } event :check, :transitions_to => :on # stay in on state otherwise end state :low_battery From cb0a0d700e959360d348957c18d5f7878779c61d Mon Sep 17 00:00:00 2001 From: Yousuf Jukaku Date: Thu, 7 Aug 2025 11:04:25 -0400 Subject: [PATCH 2/2] Allow event conditions to accept kwargs --- lib/workflow.rb | 4 +-- lib/workflow/event.rb | 6 ++-- lib/workflow/event_collection.rb | 4 +-- test/conditionals_test.rb | 62 ++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 7 deletions(-) diff --git a/lib/workflow.rb b/lib/workflow.rb index 31e639d..05863f9 100644 --- a/lib/workflow.rb +++ b/lib/workflow.rb @@ -66,7 +66,7 @@ def assign_workflow(specification_object) end define_method "can_#{event_name}?".to_sym do |*args, **kwargs| - return !!current_state.events.first_applicable(event_name, self, args) + return !!current_state.events.first_applicable(event_name, self, args, kwargs) end end end @@ -95,7 +95,7 @@ def halted_because end def process_event!(name, *args, **kwargs) - event = current_state.events.first_applicable(name, self, args) + event = current_state.events.first_applicable(name, self, args, kwargs) raise NoTransitionAllowed.new( "There is no event #{name.to_sym} defined for the #{current_state} state") \ if event.nil? diff --git a/lib/workflow/event.rb b/lib/workflow/event.rb index c425dd7..14573b6 100644 --- a/lib/workflow/event.rb +++ b/lib/workflow/event.rb @@ -15,7 +15,7 @@ def initialize(name, transitions_to, condition = nil, meta = {}, &action) end end - def condition_applicable?(object, event_arguments) + def condition_applicable?(object, event_arguments, event_kwargs) if condition if condition.is_a?(Symbol) m = object.method(condition) @@ -25,7 +25,7 @@ def condition_applicable?(object, event_arguments) if m.arity == 0 # no additional parameters accepted object.send(condition) else - object.send(condition, *event_arguments) + object.send(condition, *event_arguments, **event_kwargs) end else # Blocks and non-lambda Procs can ignore extra arguments without raising an error in Ruby, @@ -33,7 +33,7 @@ def condition_applicable?(object, event_arguments) if condition.arity == 1 # no additional parameters accepted condition.call(object) else - condition.call(object, *event_arguments) + condition.call(object, *event_arguments, **event_kwargs) end end else diff --git a/lib/workflow/event_collection.rb b/lib/workflow/event_collection.rb index 4860bf6..19ca5f6 100644 --- a/lib/workflow/event_collection.rb +++ b/lib/workflow/event_collection.rb @@ -26,9 +26,9 @@ def include?(name_or_obj) end end - def first_applicable(name, object_context, event_arguments) + def first_applicable(name, object_context, event_arguments, event_kwargs) (self[name] || []).detect do |event| - event.condition_applicable?(object_context, event_arguments) && event + event.condition_applicable?(object_context, event_arguments, event_kwargs) && event end end diff --git a/test/conditionals_test.rb b/test/conditionals_test.rb index 6f9fae3..7bb27fd 100644 --- a/test/conditionals_test.rb +++ b/test/conditionals_test.rb @@ -112,5 +112,67 @@ def initialize(battery) assert device.on? end + test 'conditionals can accept keyword arguments' do + c = Class.new do + include Workflow + + workflow do + state :inside do + # method with kwarg + event :leave, :transitions_to => :outside, if: :warm_outside? + end + state :outside do + # method with positional arg and kwarg + event :go_inside, :transitions_to => :in_office, if: :work_to_be_done? + end + state :in_office do + # Lambda with kwarg + event :relax, :transitions_to => :on_couch, if: -> (obj, hour:) { hour > 18 } + end + state :on_couch do + # Lambda with positional arg and kwarg + event :sleep, :transitions_to => :in_bed, if: -> (obj, sleepiness, hour:) { sleepiness > 10 && hour > 20 } + end + state :in_bed do + # Proc/block with no arg + event :wake_up, transitions_to: :awake, if: proc { |obj| true } + end + state :awake do + # Proc/block with kwarg + event :make_coffee, transitions_to: :caffienated, if: proc { |obj, decaf:| decaf } + end + state :caffienated + end + + def warm_outside?(outside_temperature:) + outside_temperature > 20 + end + + def work_to_be_done?(tasks_completed, quota:) + tasks_completed < quota + end + end + + obj = c.new + + obj.leave!(outside_temperature: 21) + assert obj.outside? + + obj.go_inside!(5, quota: 10) + assert obj.in_office? + + obj.relax!(hour: 19) + assert obj.on_couch? + + obj.sleep!(11, hour: 21) + assert obj.in_bed? + + obj.wake_up!(hour: 9) + assert obj.awake? + + obj.make_coffee!(decaf: true) + assert obj.caffienated? + end + end