# Magic! module BusinessFlow # More magic! # Look at use of class attrs # call! to raise errors # Hooks for cross cutting concerns # Figure out how much we can freeze # Check output slots even if they're not going through our defined setter # Conditional steps? # ActiveSupport notifiers class Base include ActiveModel::Validations class << self alias invariant validates end class_attribute :step_queue, :requirements attr_reader :parameter_object private :parameter_object def self.add_requirement(fields) @requirements ||= [] @requirements.concat(fields) fields.each do |field| validates_with NotNilValidator, attributes: [field] end end def self.needs(*fields) add_requirement(fields) wants(*fields) end def self.wants(*fields) fields.each { |field| add_field(field) } end def self.provides(*fields) attr_reader(*fields) end def self.expects(field, options = {}) validates field, options.merge(on: field) end def self.add_field(field) define_method field do if parameter_object.is_a?(Hash) && parameter_object.key?(field) parameter_object[field] else parameter_object.public_send(field) end end end def self.steps(*step_queue) self.step_queue = step_queue.map do |step| if !step.is_a?(Step) Step.new(step, {}, {}, self) else step end end end def self.step(klass, inputs = {}, outputs = {}) create_fields_for_step_outputs(outputs) Step.new(klass, inputs, outputs, self) end def self.create_fields_for_step_outputs(outputs) outputs.values.select { |field| field.is_a?(Symbol) }.map do |field| attr_reader field create_setter(field) end end def self.create_setter(field) define_method "#{field}=" do |new_value| instance_variable_set("@#{field}", new_value) valid?(field) new_value end end def self.call(parameter_object) new(parameter_object).tap(&:call) end def initialize(parameter_object) @parameter_object = parameter_object end def call return if invalid? process_steps end def merge_errors_into(other_errors) errors.each do |attribute, message| attribute = "#{self.class.name.underscore}.#{attribute}" (other_errors[attribute] << message).uniq! end end private def process_steps steps.each do |step_name| process_step(step_name) break if errors.any? end end def process_step(step) input_object = marshall_input_object(step.inputs) result = step.dispatch(self, input_object) marshall_outputs(result, step.outputs) if result.present? end def marshall_input_object(input_object) return self if input_object.blank? Hash[input_object.map do |input_name, input_value| [ input_name, process_input(input_value) ] end ] end def marshall_outputs(result, output_object) result.merge_errors_into(errors) return if errors.any? || output_object.blank? output_object.each do |(output_name, output_setter)| output = result.public_send(output_name) process_output(output, output_setter) break if errors.any? end end def process_input(input_value) case input_value when Symbol send(input_value) when Proc instance_exec(&input_value) else input_value end end def process_output(output, output_setter) case output_setter when Symbol send("#{output_setter}=", output) when Proc instance_exec(output, &output_setter) end end def steps self.class.step_queue || [] end end end