module BusinessFlow # Step is a conditional callable which can marshal its own inputs, and # returns a value which can marshal errors and outputs into a given object. class Step # Represents inputs needed to execute a step. class Inputs attr_reader :inputs def initialize(inputs) @inputs = inputs end def parameters_from_source(source) return source if inputs.blank? Hash[inputs.map do |input_name, input_value| [ input_name, Inputs.process_input(source, input_value) ] end] end def self.process_input(source, input_value) case input_value when Symbol source.send(input_value) when Proc source.instance_exec(&input_value) else input_value end end end # Represents the result of a step, and allows setting response values on # an object, and merging error data into the same object. class Result def initialize(result, output_map, output_callable) @result = result @output_map = output_map @output_callable = output_callable @result_errors = begin result.errors rescue NoMethodError nil end end def merge_into(object) merge_errors_into(object.errors) merge_outputs_into(object) end def executed? true end def errors? @result_errors.present? end def output @output_callable.call(@result) end def self.process_output(object, output, output_setter) case output_setter when Symbol object.send("#{output_setter}=", output) when Proc object.instance_exec(output, &output_setter) end end private def merge_errors_into(errors) return if @result_errors.blank? @result_errors.each do |attribute, message| attribute = "#{@result.class.name.underscore}.#{attribute}" (errors[attribute] << message).uniq! end throw :halt_step end def merge_outputs_into(object) @output_map.each do |(output_name, output_setter)| output = @result.public_send(output_name) Result.process_output(object, output, output_setter) end end end # Returned if our conditional check failed. Does nothing. class ConditionFailedResult def executed? false end def errors? false end def merge_into(_object); end end # Manage creating results for our step ResultFactory = Struct.new(:outputs, :output_callable) do def result(step_result) Result.new(step_result, outputs, output_callable) end end def initialize(callable, opts) @callable = callable @input_object = Inputs.new(opts[:inputs] || {}) outputs = opts[:outputs] || {} output = opts[:output] || ->(result) { result } @result_factory = ResultFactory.new(outputs, output) @condition = opts[:condition] || proc { true } end def call(parameter_source) parameters = @input_object.parameters_from_source(parameter_source) if @condition.call(parameter_source, parameters) @result_factory.result(@callable.call(parameter_source, parameters)) else ConditionFailedResult.new end end def inputs @input_object.inputs end def outputs @result_factory.outputs end def to_s @callable.to_s end end end