module BusinessFlow # Core DSL for BusinessFlow. The relevant methods are all in # ClassMethods. module DSL # Contains the DSL for BusinessFlow # The class that includes this must implement a parameter_object reader # which returns a hash or object representing the parameters the flow # was initialized with. The provided .call will instantiate the including # class with a parameter_object as the only argument. module ClassMethods # Requires that a field be retrievable from the initialization parameters # # This will only require that the field is not nil. The field may still # be #empty? # # @param fields The fields required from the initialization parameters def needs(*fields) @needs ||= [] return @needs if fields.blank? @needs.push(*fields) fields.each do |field| PrivateHelpers.create_parameter_field(self, field) end end # Allows a field to be retrieved from the initialiaztion parameters def wants(field, default = proc { nil }, opts = {}) internal_name = "wants_#{field}".to_sym uses(internal_name, default, opts) PrivateHelpers.create_parameter_field(self, field, internal_name) end # Declares that you will expose a field to the outside world. def provides(*fields) @provides ||= [] return @provides if fields.blank? @provides.push(*fields) fields.each { |field| PrivateHelpers.create_field(self, field) } end # Declares that you expect to set this field during the course of # processing, and that it should meet the given ActiveModel # validations. def expects(field, options = {}) validates field, options.merge(on: field) PrivateHelpers.create_field(self, field) end def uses(field, klass, opts = {}) callable = Callable.new(klass, self) step = Step.new(callable, opts) PrivateHelpers.create_memoized_field(self, field, step) private field end def step(klass, opts = {}) callable = Callable.new(klass, self) opts = opts.merge( condition: PrivateHelpers.create_conditional_callable(self, opts) ) step_queue << step = Step.new(callable, opts) step.outputs.values.each do |field| PrivateHelpers.create_field(self, field) end end def call(parameter_object) new(parameter_object).tap(&:call) end def call!(*args) ret = call(*args) raise FlowFailedException, ret if ret.errors.any? ret end def step_queue @step_queue ||= [] end end def self.included(klass) # That we include ActiveModel::Validations is considered part of our # public API, even though we provide our own aliases. klass.include(ActiveModel::Validations) klass.extend(ClassMethods) klass.instance_eval do class << self # See above -- that this is an alias is considered public API. alias invariant validates end validates_with NotNilValidator end end # Keep our internal helpers in a different module to avoid polluting the # namespace of whoever includes us. module PrivateHelpers # Handle some logic around conditions class ConditionList def initialize(if_stmts, unless_stmts, klass) @klass = klass @conditions = Array.wrap(if_stmts).map(&method(:to_if)) + Array.wrap(unless_stmts).map(&method(:to_unless)) end def call(instance, inputs) @conditions.all? { |cond| cond.call(instance, inputs) } end private def to_if(cond) Callable.new(cond, @klass) end def to_unless(cond) if_stmt = to_if(cond) unless_stmt = proc do |instance, input| !if_stmt.call(instance, input) end to_if(unless_stmt) end end def self.create_parameter_field(klass, field, fallback = nil) klass.send(:define_method, field, ¶meter_proc(field, fallback)) klass.send(:private, field) end def self.read_from_parameter_object(field) proc 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.parameter_proc(field, fallback) read = read_from_parameter_object(field) proc do begin instance_exec(&read) rescue NoMethodError nil end || (fallback && send(fallback)) end end def self.create_conditional_callable(klass, opts) return unless opts[:if] || opts[:unless] Callable.new(condition(klass, opts), klass) end def self.condition(klass, opts) if_stmts = opts.fetch(:if, proc { true }) unless_stmts = opts.fetch(:unless, proc { false }) ConditionList.new(if_stmts, unless_stmts, klass) end def self.create_field(klass, field) return unless field.is_a?(Symbol) define_getter(klass, field) setter_name = "#{field}=".to_sym define_setter(klass, setter_name, field) klass.send(:private, setter_name) end def self.define_getter(klass, field) return if klass.method_defined?(field) || klass.private_method_defined?(field) klass.send(:attr_reader, field) end def self.define_setter(klass, setter_name, field) return if klass.method_defined?(setter_name) || klass.private_method_defined?(setter_name) klass.send(:define_method, setter_name, &setter_proc("@#{field}", field)) end def self.safe_ivar_name(field) ivar_name = "@business_flow_dsl_#{field}" if ivar_name.end_with?('?') ivar_name.sub!(/\?$/, '_query') elsif ivar_name.end_with?('!') ivar_name.sub!(/\!$/, '_bang') end ivar_name.to_sym end def self.setter_proc(ivar_name, field) proc do |new_value| instance_variable_set(ivar_name, new_value) throw :halt_step unless valid?(field) new_value end end def self.create_memoized_field(klass, field, step) ivar_name = safe_ivar_name(field) setter_proc = self.setter_proc(ivar_name, field) klass.send(:define_method, field) do if instance_variable_defined?(ivar_name) instance_variable_get(ivar_name) else instance_exec(step.call(self).output, &setter_proc) end end end end end end