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) validates_with NotNilValidator, attributes: fields wants(*fields) end # Allows a field to be retrieved from the initialiaztoin parameters # # Unlike needs, this applies no validation to the field. It may # be nil. # # @param (see #needs) def wants(*fields) fields.each do |field| PrivateHelpers.create_parameter_field(self, field) end end # Declares that you will expose a field to the outside world. def provides(*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) provides 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) provides(*step.outputs.values) end def call(parameter_object) new(parameter_object).tap(&:call) 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 end end # Keep our internal helpers in a different module to avoid polluting the # namespace of whoever includes us. module PrivateHelpers def self.create_parameter_field(klass, field) klass.send(: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.create_conditional_callable(klass, opts) condition = opts[:if] condition && Callable.new(condition, klass) end def self.create_field(klass, field) return unless field.is_a?(Symbol) klass.send(:attr_reader, field) setter_name = "#{field}=".to_sym define_setter(klass, setter_name, field) klass.send(:private, setter_name) 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) do |new_value| instance_variable_set("@#{field}".to_sym, new_value) throw :halt_step unless valid?(field) new_value end end end end end