Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/workflow.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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?
Expand Down
14 changes: 9 additions & 5 deletions lib/workflow/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -25,12 +25,16 @@ 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
# 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, **event_kwargs)
end
end
else
true
Expand Down
4 changes: 2 additions & 2 deletions lib/workflow/event_collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
65 changes: 64 additions & 1 deletion test/conditionals_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -111,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