business_flow 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/lib/business_flow/base.rb +1 -32
- data/lib/business_flow/cacheable.rb +24 -26
- data/lib/business_flow/callable.rb +59 -33
- data/lib/business_flow/default_step_executor.rb +6 -33
- data/lib/business_flow/dsl.rb +227 -134
- data/lib/business_flow/instrument.rb +17 -0
- data/lib/business_flow/instrumented_executor.rb +28 -0
- data/lib/business_flow/instrumented_step_executor.rb +23 -0
- data/lib/business_flow/step.rb +54 -13
- data/lib/business_flow/validations.rb +25 -0
- data/lib/business_flow/version.rb +1 -1
- data/lib/business_flow.rb +4 -1
- metadata +6 -3
- data/lib/business_flow/not_nil_validator.rb +0 -14
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 430484fefee99d40a5f0043d018409179459cf87
         | 
| 4 | 
            +
              data.tar.gz: 28aa9f6aabb03d731824498e96c00d7bb23aa224
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 2bb43af5b2ff636c7e1cd78cf6b6b0e5c4211969c5edac41d999a75f6e3f768921053dd6c4c4acd8119de9f61a290636095fc784fbc3af19a188ccc1577d2d9d
         | 
| 7 | 
            +
              data.tar.gz: 775da5c4e2a71d925e7963d34ec97e78773fd69bf057e85f476b578788740ea7bf7f25832bd0ff9b89a9f1336ee60f114b208e9ef1071bf620ef99717df87cb0
         | 
    
        data/Gemfile.lock
    CHANGED
    
    
    
        data/lib/business_flow/base.rb
    CHANGED
    
    | @@ -1,40 +1,9 @@ | |
| 1 1 | 
             
            module BusinessFlow
         | 
| 2 2 | 
             
              # include BusinessFlow::Base in any object to get a flow!
         | 
| 3 | 
            -
              # :reek:ModuleInitialize
         | 
| 4 3 | 
             
              module Base
         | 
| 5 4 | 
             
                def self.included(klass)
         | 
| 6 5 | 
             
                  klass.include(DSL)
         | 
| 7 | 
            -
                  klass. | 
| 8 | 
            -
                end
         | 
| 9 | 
            -
             | 
| 10 | 
            -
                # "Helper" methods which we put on an object to provide some default
         | 
| 11 | 
            -
                # behavior.
         | 
| 12 | 
            -
                module InstanceMethods
         | 
| 13 | 
            -
                  attr_reader :parameter_object
         | 
| 14 | 
            -
                  private :parameter_object
         | 
| 15 | 
            -
             | 
| 16 | 
            -
                  # Initialize is here so that we can set the parameter object, and then
         | 
| 17 | 
            -
                  # allow our "parent" class to handle any additional initialization that
         | 
| 18 | 
            -
                  # it needs to do.
         | 
| 19 | 
            -
                  def initialize(parameter_object)
         | 
| 20 | 
            -
                    @parameter_object = parameter_object
         | 
| 21 | 
            -
                    catch(:halt_step) do
         | 
| 22 | 
            -
                      super()
         | 
| 23 | 
            -
                    end
         | 
| 24 | 
            -
                  end
         | 
| 25 | 
            -
             | 
| 26 | 
            -
                  def call
         | 
| 27 | 
            -
                    # If our initialization process set any errors, return
         | 
| 28 | 
            -
                    return if errors.any? || invalid?
         | 
| 29 | 
            -
                    if defined?(super)
         | 
| 30 | 
            -
                      catch(:halt_step) do
         | 
| 31 | 
            -
                        super
         | 
| 32 | 
            -
                      end
         | 
| 33 | 
            -
                    else
         | 
| 34 | 
            -
                      ::BusinessFlow::DefaultStepExecutor.new(self.class.step_queue, self)
         | 
| 35 | 
            -
                                                         .call
         | 
| 36 | 
            -
                    end
         | 
| 37 | 
            -
                  end
         | 
| 6 | 
            +
                  klass.include(Validations)
         | 
| 38 7 | 
             
                end
         | 
| 39 8 | 
             
              end
         | 
| 40 9 | 
             
            end
         | 
| @@ -13,6 +13,21 @@ module BusinessFlow | |
| 13 13 |  | 
| 14 14 | 
             
                # DSL Methods
         | 
| 15 15 | 
             
                module ClassMethods
         | 
| 16 | 
            +
                  # Responsible for converting our DSL options into cache store options
         | 
| 17 | 
            +
                  CacheOptions = Struct.new(:ttl) do
         | 
| 18 | 
            +
                    def to_store_options
         | 
| 19 | 
            +
                      # compact is not available in Ruby <2.4 or ActiveSupport < 4, so
         | 
| 20 | 
            +
                      # we can't use it here.
         | 
| 21 | 
            +
                      options = {}
         | 
| 22 | 
            +
                      options[:expires_in] = ttl if ttl
         | 
| 23 | 
            +
                      options
         | 
| 24 | 
            +
                    end
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  def cache_options
         | 
| 28 | 
            +
                    @cache_options ||= CacheOptions.new
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 16 31 | 
             
                  def cache_store(store = nil)
         | 
| 17 32 | 
             
                    if store
         | 
| 18 33 | 
             
                      @cache_store = store
         | 
| @@ -27,45 +42,28 @@ module BusinessFlow | |
| 27 42 |  | 
| 28 43 | 
             
                  def cache_ttl(ttl = nil)
         | 
| 29 44 | 
             
                    if ttl
         | 
| 30 | 
            -
                       | 
| 45 | 
            +
                      cache_options.ttl = ttl
         | 
| 31 46 | 
             
                    else
         | 
| 32 | 
            -
                       | 
| 47 | 
            +
                      cache_options.ttl
         | 
| 33 48 | 
             
                    end
         | 
| 34 49 | 
             
                  end
         | 
| 35 50 |  | 
| 36 51 | 
             
                  def cache_key(key = nil)
         | 
| 37 52 | 
             
                    if key
         | 
| 38 | 
            -
                      @cache_key = Callable.new(key | 
| 53 | 
            +
                      @cache_key = Callable.new(key)
         | 
| 39 54 | 
             
                    else
         | 
| 40 | 
            -
                      @cache_key ||= Callable.new(:parameter_object | 
| 55 | 
            +
                      @cache_key ||= Callable.new(:parameter_object)
         | 
| 41 56 | 
             
                    end
         | 
| 42 57 | 
             
                  end
         | 
| 43 58 |  | 
| 44 | 
            -
                  def  | 
| 45 | 
            -
                    flow  | 
| 46 | 
            -
             | 
| 47 | 
            -
                  rescue FlowFailedException
         | 
| 48 | 
            -
                    flow
         | 
| 49 | 
            -
                  end
         | 
| 50 | 
            -
                end
         | 
| 51 | 
            -
             | 
| 52 | 
            -
                # Avoid polluting the namespace of whoever includes us
         | 
| 53 | 
            -
                module PrivateHelpers
         | 
| 54 | 
            -
                  def self.execute(klass, flow)
         | 
| 55 | 
            -
                    klass.cache_store.fetch(flow.cache_key, cache_options(klass)) do
         | 
| 56 | 
            -
                      flow.call
         | 
| 59 | 
            +
                  def execute(flow)
         | 
| 60 | 
            +
                    cache_store.fetch(flow.cache_key, cache_options.to_store_options) do
         | 
| 61 | 
            +
                      super(flow)
         | 
| 57 62 | 
             
                      raise FlowFailedException, flow if flow.errors.any?
         | 
| 58 63 | 
             
                      flow
         | 
| 59 64 | 
             
                    end
         | 
| 60 | 
            -
                   | 
| 61 | 
            -
             | 
| 62 | 
            -
                  def self.cache_options(klass)
         | 
| 63 | 
            -
                    # compact is not available in Ruby <2.4 or ActiveSupport < 4, so
         | 
| 64 | 
            -
                    # we can't use it here.
         | 
| 65 | 
            -
                    options = {}
         | 
| 66 | 
            -
                    ttl = klass.cache_ttl
         | 
| 67 | 
            -
                    options[:expires_in] = ttl if ttl
         | 
| 68 | 
            -
                    options
         | 
| 65 | 
            +
                  rescue FlowFailedException
         | 
| 66 | 
            +
                    flow
         | 
| 69 67 | 
             
                  end
         | 
| 70 68 | 
             
                end
         | 
| 71 69 | 
             
              end
         | 
| @@ -3,14 +3,21 @@ module BusinessFlow | |
| 3 3 | 
             
              # method, a symbol representing another class which implements .call, or a
         | 
| 4 4 | 
             
              # proc/lambda
         | 
| 5 5 | 
             
              class Callable
         | 
| 6 | 
            -
                def initialize(callable | 
| 7 | 
            -
                  @metaclass = metaclass
         | 
| 6 | 
            +
                def initialize(callable)
         | 
| 8 7 | 
             
                  @callable = callable
         | 
| 9 8 | 
             
                  check_callable
         | 
| 10 9 | 
             
                end
         | 
| 11 10 |  | 
| 11 | 
            +
                # :reek:ManualDispatch
         | 
| 12 12 | 
             
                def call(instance, inputs)
         | 
| 13 | 
            -
                   | 
| 13 | 
            +
                  if instance.respond_to?(@callable, true)
         | 
| 14 | 
            +
                    send_callable(instance, inputs)
         | 
| 15 | 
            +
                  else
         | 
| 16 | 
            +
                    @callable = lookup_callable(instance) ||
         | 
| 17 | 
            +
                                raise(NameError, "undefined constant #{@callable}")
         | 
| 18 | 
            +
                    check_callable
         | 
| 19 | 
            +
                    call(instance, inputs)
         | 
| 20 | 
            +
                  end
         | 
| 14 21 | 
             
                end
         | 
| 15 22 |  | 
| 16 23 | 
             
                def to_s
         | 
| @@ -19,55 +26,74 @@ module BusinessFlow | |
| 19 26 |  | 
| 20 27 | 
             
                private
         | 
| 21 28 |  | 
| 22 | 
            -
                 | 
| 29 | 
            +
                def send_callable(instance, inputs)
         | 
| 30 | 
            +
                  instance_eval %{
         | 
| 31 | 
            +
                    def call(instance, _inputs)
         | 
| 32 | 
            +
                      instance.send(@callable)
         | 
| 33 | 
            +
                    end
         | 
| 34 | 
            +
                  }, __FILE__, __LINE__ - 4
         | 
| 35 | 
            +
                  call(instance, inputs)
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
             | 
| 23 38 | 
             
                def check_callable
         | 
| 24 39 | 
             
                  if @callable.is_a?(Proc)
         | 
| 25 | 
            -
                     | 
| 26 | 
            -
                   | 
| 27 | 
            -
                     | 
| 28 | 
            -
                   | 
| 40 | 
            +
                    proc_callable
         | 
| 41 | 
            +
                  else
         | 
| 42 | 
            +
                    call_callable
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
                rescue NameError
         | 
| 45 | 
            +
                  unless @callable.is_a?(Symbol)
         | 
| 29 46 | 
             
                    raise ArgumentError, 'callable must be a symbol or respond to #call'
         | 
| 30 47 | 
             
                  end
         | 
| 31 48 | 
             
                end
         | 
| 32 49 |  | 
| 33 | 
            -
                def  | 
| 34 | 
            -
                   | 
| 35 | 
            -
                     | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 39 | 
            -
                      @callable = lookup_callable ||
         | 
| 40 | 
            -
                                  raise(NameError, "undefined constant #{@callable}")
         | 
| 41 | 
            -
                      check_callable
         | 
| 50 | 
            +
                def proc_callable
         | 
| 51 | 
            +
                  instance_eval %{
         | 
| 52 | 
            +
                    def call(instance, inputs)
         | 
| 53 | 
            +
                      instance.instance_exec(
         | 
| 54 | 
            +
                        #{@callable.arity.zero? ? '' : 'inputs, '}&@callable
         | 
| 55 | 
            +
                      )
         | 
| 42 56 | 
             
                    end
         | 
| 57 | 
            +
                  }, __FILE__, __LINE__ - 6
         | 
| 43 58 | 
             
                end
         | 
| 44 59 |  | 
| 45 | 
            -
                def  | 
| 46 | 
            -
                   | 
| 47 | 
            -
             | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 60 | 
            +
                def call_callable
         | 
| 61 | 
            +
                  case @callable.method(:call).arity
         | 
| 62 | 
            +
                  when 1, -1
         | 
| 63 | 
            +
                    single_inputs_callable
         | 
| 64 | 
            +
                  when 0
         | 
| 65 | 
            +
                    zero_inputs_callable
         | 
| 66 | 
            +
                  else two_inputs_callable
         | 
| 52 67 | 
             
                  end
         | 
| 53 68 | 
             
                end
         | 
| 54 69 |  | 
| 55 | 
            -
                def  | 
| 56 | 
            -
                   | 
| 57 | 
            -
                     | 
| 58 | 
            -
                    when 1, -1
         | 
| 70 | 
            +
                def single_inputs_callable
         | 
| 71 | 
            +
                  instance_eval %{
         | 
| 72 | 
            +
                    def call(_instance, inputs)
         | 
| 59 73 | 
             
                      @callable.call(inputs)
         | 
| 60 | 
            -
                     | 
| 74 | 
            +
                    end
         | 
| 75 | 
            +
                  }, __FILE__, __LINE__ - 4
         | 
| 76 | 
            +
                end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                def zero_inputs_callable
         | 
| 79 | 
            +
                  instance_eval %{
         | 
| 80 | 
            +
                    def call(_instance, _inputs)
         | 
| 61 81 | 
             
                      @callable.call
         | 
| 62 | 
            -
                     | 
| 82 | 
            +
                    end
         | 
| 83 | 
            +
                  }, __FILE__, __LINE__ - 4
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                def two_inputs_callable
         | 
| 87 | 
            +
                  instance_eval %{
         | 
| 88 | 
            +
                    def call(instance, inputs)
         | 
| 63 89 | 
             
                      @callable.call(instance, inputs)
         | 
| 64 90 | 
             
                    end
         | 
| 65 | 
            -
                   | 
| 91 | 
            +
                  }, __FILE__, __LINE__ - 4
         | 
| 66 92 | 
             
                end
         | 
| 67 93 |  | 
| 68 | 
            -
                def lookup_callable
         | 
| 94 | 
            +
                def lookup_callable(first_instance)
         | 
| 69 95 | 
             
                  constant_name = @callable.to_s.camelcase
         | 
| 70 | 
            -
                   | 
| 96 | 
            +
                  first_instance.class.parents.each do |parent|
         | 
| 71 97 | 
             
                    begin
         | 
| 72 98 | 
             
                      return parent.const_get(constant_name)
         | 
| 73 99 | 
             
                    rescue NameError
         | 
| @@ -9,49 +9,22 @@ module BusinessFlow | |
| 9 9 | 
             
                end
         | 
| 10 10 |  | 
| 11 11 | 
             
                def call
         | 
| 12 | 
            -
                   | 
| 13 | 
            -
             | 
| 14 | 
            -
                     | 
| 15 | 
            -
                      break unless process_step(step)
         | 
| 16 | 
            -
                    end
         | 
| 12 | 
            +
                  @step_queue.each do |step|
         | 
| 13 | 
            +
                    break if @flow.errors?
         | 
| 14 | 
            +
                    execute_step(step)
         | 
| 17 15 | 
             
                  end
         | 
| 18 16 | 
             
                end
         | 
| 19 17 |  | 
| 20 18 | 
             
                protected
         | 
| 21 19 |  | 
| 22 | 
            -
                 | 
| 23 | 
            -
                  catch(:halt_step) { execute_step(step) }
         | 
| 24 | 
            -
                  return true if @flow.errors.blank?
         | 
| 25 | 
            -
                  ActiveSupport::Notifications.publish(
         | 
| 26 | 
            -
                    event_name(step) + '.error', step: step, flow: @flow
         | 
| 27 | 
            -
                  )
         | 
| 28 | 
            -
                  false
         | 
| 29 | 
            -
                end
         | 
| 20 | 
            +
                attr_reader :flow, :step_queue
         | 
| 30 21 |  | 
| 31 22 | 
             
                def execute_step(step)
         | 
| 32 | 
            -
                   | 
| 33 | 
            -
                     | 
| 34 | 
            -
                  ) do |payload|
         | 
| 35 | 
            -
                    payload[:step_result] = result = step.call(@flow)
         | 
| 23 | 
            +
                  catch(:halt_step) do
         | 
| 24 | 
            +
                    result = step.call(@flow)
         | 
| 36 25 | 
             
                    result.merge_into(@flow)
         | 
| 37 26 | 
             
                    result
         | 
| 38 27 | 
             
                  end
         | 
| 39 28 | 
             
                end
         | 
| 40 | 
            -
             | 
| 41 | 
            -
                def flow_name
         | 
| 42 | 
            -
                  @flow_name ||= @flow.class.to_s.underscore
         | 
| 43 | 
            -
                end
         | 
| 44 | 
            -
             | 
| 45 | 
            -
                def flow_event_name
         | 
| 46 | 
            -
                  @flow_event_name ||= "business_flow.flow.#{flow_name}"
         | 
| 47 | 
            -
                end
         | 
| 48 | 
            -
             | 
| 49 | 
            -
                def step_event_name(step)
         | 
| 50 | 
            -
                  "#{flow_name}.#{step.to_s.underscore}"
         | 
| 51 | 
            -
                end
         | 
| 52 | 
            -
             | 
| 53 | 
            -
                def event_name(step)
         | 
| 54 | 
            -
                  "business_flow.step.#{step_event_name(step)}"
         | 
| 55 | 
            -
                end
         | 
| 56 29 | 
             
              end
         | 
| 57 30 | 
             
            end
         | 
    
        data/lib/business_flow/dsl.rb
    CHANGED
    
    | @@ -3,10 +3,6 @@ module BusinessFlow | |
| 3 3 | 
             
              # ClassMethods.
         | 
| 4 4 | 
             
              module DSL
         | 
| 5 5 | 
             
                # Contains the DSL for BusinessFlow
         | 
| 6 | 
            -
                # The class that includes this must implement a parameter_object reader
         | 
| 7 | 
            -
                # which returns a hash or object representing the parameters the flow
         | 
| 8 | 
            -
                # was initialized with. The provided .call will instantiate the including
         | 
| 9 | 
            -
                # class with a parameter_object as the only argument.
         | 
| 10 6 | 
             
                module ClassMethods
         | 
| 11 7 | 
             
                  # Requires that a field be retrievable from the initialization parameters
         | 
| 12 8 | 
             
                  #
         | 
| @@ -15,200 +11,297 @@ module BusinessFlow | |
| 15 11 | 
             
                  #
         | 
| 16 12 | 
             
                  # @param fields The fields required from the initialization parameters
         | 
| 17 13 | 
             
                  def needs(*fields)
         | 
| 18 | 
            -
                    @needs ||= []
         | 
| 19 | 
            -
                     | 
| 20 | 
            -
                    @needs.push(*fields)
         | 
| 21 | 
            -
                    fields.each do |field|
         | 
| 22 | 
            -
                      PrivateHelpers.create_parameter_field(self, field)
         | 
| 23 | 
            -
                    end
         | 
| 14 | 
            +
                    @needs ||= FieldList.new([], ParameterField, self)
         | 
| 15 | 
            +
                    @needs.add_fields(fields)
         | 
| 24 16 | 
             
                  end
         | 
| 25 17 |  | 
| 26 18 | 
             
                  # Allows a field to be retrieved from the initialiaztion parameters
         | 
| 27 19 | 
             
                  def wants(field, default = proc { nil }, opts = {})
         | 
| 28 20 | 
             
                    internal_name = "wants_#{field}".to_sym
         | 
| 29 21 | 
             
                    uses(internal_name, default, opts)
         | 
| 30 | 
            -
                     | 
| 22 | 
            +
                    ParameterField.new(field, internal_name).add_to(self)
         | 
| 31 23 | 
             
                  end
         | 
| 32 24 |  | 
| 33 25 | 
             
                  # Declares that you will expose a field to the outside world.
         | 
| 34 26 | 
             
                  def provides(*fields)
         | 
| 35 | 
            -
                    @provides ||= []
         | 
| 36 | 
            -
                     | 
| 37 | 
            -
                    @provides.push(*fields)
         | 
| 38 | 
            -
                    fields.each { |field| PrivateHelpers.create_field(self, field) }
         | 
| 39 | 
            -
                  end
         | 
| 40 | 
            -
             | 
| 41 | 
            -
                  # Declares that you expect to set this field during the course of
         | 
| 42 | 
            -
                  # processing, and that it should meet the given ActiveModel
         | 
| 43 | 
            -
                  # validations.
         | 
| 44 | 
            -
                  def expects(field, options = {})
         | 
| 45 | 
            -
                    validates field, options.merge(on: field)
         | 
| 46 | 
            -
                    PrivateHelpers.create_field(self, field)
         | 
| 27 | 
            +
                    @provides ||= FieldList.new([], PublicField, self)
         | 
| 28 | 
            +
                    @provides.add_fields(fields)
         | 
| 47 29 | 
             
                  end
         | 
| 48 30 |  | 
| 49 31 | 
             
                  def uses(field, klass, opts = {})
         | 
| 50 | 
            -
                     | 
| 51 | 
            -
                     | 
| 52 | 
            -
                     | 
| 53 | 
            -
                    private field
         | 
| 32 | 
            +
                    step = Step.new(Callable.new(klass), opts)
         | 
| 33 | 
            +
                    retriever = proc { step.call(self).output }
         | 
| 34 | 
            +
                    UsesField.new(field, retriever).add_to(self)
         | 
| 54 35 | 
             
                  end
         | 
| 55 36 |  | 
| 56 37 | 
             
                  def step(klass, opts = {})
         | 
| 57 | 
            -
                     | 
| 58 | 
            -
                     | 
| 59 | 
            -
             | 
| 60 | 
            -
                    )
         | 
| 61 | 
            -
                    step_queue << step = Step.new(callable, opts)
         | 
| 62 | 
            -
                    step.outputs.values.each do |field|
         | 
| 63 | 
            -
                      PrivateHelpers.create_field(self, field)
         | 
| 64 | 
            -
                    end
         | 
| 38 | 
            +
                    step = Step.new(Callable.new(klass), opts)
         | 
| 39 | 
            +
                    step_queue.push(step)
         | 
| 40 | 
            +
                    step.outputs.values.each { |field| Field.new(field).add_to(self) }
         | 
| 65 41 | 
             
                  end
         | 
| 66 42 |  | 
| 67 43 | 
             
                  def call(parameter_object)
         | 
| 68 | 
            -
                     | 
| 44 | 
            +
                    allocate.tap do |flow|
         | 
| 45 | 
            +
                      catch(:halt_step) do
         | 
| 46 | 
            +
                        flow.send(:_business_flow_dsl_initialize,
         | 
| 47 | 
            +
                                  ParameterObject.new(parameter_object), needs)
         | 
| 48 | 
            +
                        execute(flow)
         | 
| 49 | 
            +
                      end
         | 
| 50 | 
            +
                    end
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  # :reek:UtilityFunction
         | 
| 54 | 
            +
                  def execute(flow)
         | 
| 55 | 
            +
                    flow.call
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  def build(parameter_object)
         | 
| 59 | 
            +
                    call(parameter_object)
         | 
| 69 60 | 
             
                  end
         | 
| 70 61 |  | 
| 71 62 | 
             
                  def call!(*args)
         | 
| 72 | 
            -
                     | 
| 73 | 
            -
                    raise FlowFailedException,  | 
| 74 | 
            -
                     | 
| 63 | 
            +
                    flow = call(*args)
         | 
| 64 | 
            +
                    raise FlowFailedException, flow if flow.errors.any?
         | 
| 65 | 
            +
                    flow
         | 
| 75 66 | 
             
                  end
         | 
| 76 67 |  | 
| 77 68 | 
             
                  def step_queue
         | 
| 78 69 | 
             
                    @step_queue ||= []
         | 
| 79 70 | 
             
                  end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  def step_executor(executor_class = nil)
         | 
| 73 | 
            +
                    if executor_class
         | 
| 74 | 
            +
                      @executor_class = executor_class
         | 
| 75 | 
            +
                    else
         | 
| 76 | 
            +
                      @executor_class ||= ::BusinessFlow::DefaultStepExecutor
         | 
| 77 | 
            +
                    end
         | 
| 78 | 
            +
                  end
         | 
| 80 79 | 
             
                end
         | 
| 81 80 |  | 
| 82 81 | 
             
                def self.included(klass)
         | 
| 83 | 
            -
                  # That we include ActiveModel::Validations is considered part of our
         | 
| 84 | 
            -
                  # public API, even though we provide our own aliases.
         | 
| 85 | 
            -
                  klass.include(ActiveModel::Validations)
         | 
| 86 82 | 
             
                  klass.extend(ClassMethods)
         | 
| 87 | 
            -
             | 
| 88 | 
            -
             | 
| 89 | 
            -
             | 
| 90 | 
            -
             | 
| 83 | 
            +
                end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                attr_reader :parameter_object
         | 
| 86 | 
            +
                private :parameter_object
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                def call
         | 
| 89 | 
            +
                  return if invalid?
         | 
| 90 | 
            +
                  klass = self.class
         | 
| 91 | 
            +
                  klass.step_executor.new(klass.step_queue, self).call
         | 
| 92 | 
            +
                end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                # Responsible for setting the parameter object and validating inputs.
         | 
| 95 | 
            +
                # This is a method directly on the object instead of something we
         | 
| 96 | 
            +
                # handle through instance_eval/exec for performance reasons.
         | 
| 97 | 
            +
                # :reek:NilCheck
         | 
| 98 | 
            +
                private def _business_flow_dsl_initialize(parameter_object, needs)
         | 
| 99 | 
            +
                  @parameter_object = parameter_object
         | 
| 100 | 
            +
                  needs.each do |need|
         | 
| 101 | 
            +
                    if send(need).nil?
         | 
| 102 | 
            +
                      errors[need] << 'must not be nil'
         | 
| 103 | 
            +
                      throw :halt_step
         | 
| 91 104 | 
             
                    end
         | 
| 92 | 
            -
                    validates_with NotNilValidator
         | 
| 93 105 | 
             
                  end
         | 
| 106 | 
            +
                  initialize
         | 
| 94 107 | 
             
                end
         | 
| 95 108 |  | 
| 96 | 
            -
                 | 
| 97 | 
            -
             | 
| 98 | 
            -
                 | 
| 99 | 
            -
                  # Handle some logic around conditions
         | 
| 100 | 
            -
                  class ConditionList
         | 
| 101 | 
            -
                    def initialize(if_stmts, unless_stmts, klass)
         | 
| 102 | 
            -
                      @klass = klass
         | 
| 103 | 
            -
                      @conditions = Array.wrap(if_stmts).map(&method(:to_if)) +
         | 
| 104 | 
            -
                                    Array.wrap(unless_stmts).map(&method(:to_unless))
         | 
| 105 | 
            -
                    end
         | 
| 109 | 
            +
                def errors
         | 
| 110 | 
            +
                  @errors ||= ActiveModel::Errors.new(self)
         | 
| 111 | 
            +
                end
         | 
| 106 112 |  | 
| 107 | 
            -
             | 
| 108 | 
            -
             | 
| 109 | 
            -
             | 
| 113 | 
            +
                def errors?
         | 
| 114 | 
            +
                  @errors.present?
         | 
| 115 | 
            +
                end
         | 
| 110 116 |  | 
| 111 | 
            -
             | 
| 117 | 
            +
                def valid?(_context = nil)
         | 
| 118 | 
            +
                  @errors.blank?
         | 
| 119 | 
            +
                end
         | 
| 112 120 |  | 
| 113 | 
            -
             | 
| 114 | 
            -
             | 
| 115 | 
            -
             | 
| 121 | 
            +
                def invalid?(context = nil)
         | 
| 122 | 
            +
                  !valid?(context)
         | 
| 123 | 
            +
                end
         | 
| 116 124 |  | 
| 117 | 
            -
             | 
| 118 | 
            -
             | 
| 119 | 
            -
             | 
| 120 | 
            -
             | 
| 121 | 
            -
             | 
| 122 | 
            -
             | 
| 123 | 
            -
                     | 
| 125 | 
            +
                # Responsible for creating fields on a class and noting the of field
         | 
| 126 | 
            +
                class FieldList
         | 
| 127 | 
            +
                  attr_reader :field_list
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                  def initialize(field_list, field_klass, klass)
         | 
| 130 | 
            +
                    @field_list = field_list
         | 
| 131 | 
            +
                    @field_klass = field_klass
         | 
| 132 | 
            +
                    @klass = klass
         | 
| 124 133 | 
             
                  end
         | 
| 125 134 |  | 
| 126 | 
            -
                  def  | 
| 127 | 
            -
                     | 
| 128 | 
            -
                     | 
| 135 | 
            +
                  def add_fields(fields)
         | 
| 136 | 
            +
                    fields.each { |field| @field_klass.new(field).add_to(@klass) }
         | 
| 137 | 
            +
                    @field_list.concat(fields)
         | 
| 129 138 | 
             
                  end
         | 
| 139 | 
            +
                end
         | 
| 130 140 |  | 
| 131 | 
            -
             | 
| 132 | 
            -
             | 
| 133 | 
            -
             | 
| 134 | 
            -
             | 
| 135 | 
            -
             | 
| 136 | 
            -
             | 
| 141 | 
            +
                # Helper class to manage logic around adding fields
         | 
| 142 | 
            +
                class Field
         | 
| 143 | 
            +
                  def initialize(field)
         | 
| 144 | 
            +
                    @field = field
         | 
| 145 | 
            +
                    # For proc bindings.
         | 
| 146 | 
            +
                    ivar_name = instance_variable_name
         | 
| 147 | 
            +
                    @getter = ivar_name
         | 
| 148 | 
            +
                    @setter = self.class.setter_factory(field, ivar_name)
         | 
| 149 | 
            +
                  end
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                  def add_to(klass)
         | 
| 152 | 
            +
                    Field.eval_method(klass, field, getter)
         | 
| 153 | 
            +
                    Field.eval_method(klass, setter_name, setter)
         | 
| 154 | 
            +
                  end
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                  def self.eval_method(klass, name, str)
         | 
| 157 | 
            +
                    return if klass.method_defined?(name) ||
         | 
| 158 | 
            +
                              klass.private_method_defined?(name)
         | 
| 159 | 
            +
                    unsafe_eval_method(klass, name, str)
         | 
| 160 | 
            +
                  end
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                  def self.unsafe_eval_method(klass, name, str)
         | 
| 163 | 
            +
                    body = ["private def #{name}", str, 'end'].join("\n")
         | 
| 164 | 
            +
                    klass.class_eval body, __FILE__, __LINE__
         | 
| 165 | 
            +
                  end
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                  def self.setter_factory(field, ivar_name)
         | 
| 168 | 
            +
                    <<-SETTER
         | 
| 169 | 
            +
                      #{ivar_name} = new_value
         | 
| 170 | 
            +
                      throw :halt_step unless valid?(:#{field})
         | 
| 171 | 
            +
                      new_value
         | 
| 172 | 
            +
                    SETTER
         | 
| 173 | 
            +
                  end
         | 
| 174 | 
            +
             | 
| 175 | 
            +
                  private
         | 
| 176 | 
            +
             | 
| 177 | 
            +
                  attr_reader :field, :getter, :setter
         | 
| 178 | 
            +
             | 
| 179 | 
            +
                  def setter_name
         | 
| 180 | 
            +
                    @setter_name ||= "#{field}=(new_value)"
         | 
| 181 | 
            +
                  end
         | 
| 182 | 
            +
             | 
| 183 | 
            +
                  def instance_variable_name
         | 
| 184 | 
            +
                    @instance_variable_name ||= "@#{field}"
         | 
| 185 | 
            +
                  end
         | 
| 186 | 
            +
                end
         | 
| 187 | 
            +
             | 
| 188 | 
            +
                # Create a field with a public getter
         | 
| 189 | 
            +
                class PublicField
         | 
| 190 | 
            +
                  def initialize(field)
         | 
| 191 | 
            +
                    @name = field
         | 
| 192 | 
            +
                    @field = Field.new(field)
         | 
| 193 | 
            +
                  end
         | 
| 194 | 
            +
             | 
| 195 | 
            +
                  def add_to(klass)
         | 
| 196 | 
            +
                    @field.add_to(klass)
         | 
| 197 | 
            +
                    klass.send(:public, @name)
         | 
| 198 | 
            +
                  end
         | 
| 199 | 
            +
                end
         | 
| 200 | 
            +
             | 
| 201 | 
            +
                # Helper class around memoized fields
         | 
| 202 | 
            +
                class MemoizedField
         | 
| 203 | 
            +
                  def initialize(field, retriever, setter_factory)
         | 
| 204 | 
            +
                    @field = field
         | 
| 205 | 
            +
                    @retriever = retriever
         | 
| 206 | 
            +
                    @setter_factory = setter_factory
         | 
| 207 | 
            +
                  end
         | 
| 208 | 
            +
             | 
| 209 | 
            +
                  def add_to(klass)
         | 
| 210 | 
            +
                    setter = setter_factory.call(field, safe_ivar_name)
         | 
| 211 | 
            +
                    Field.unsafe_eval_method(
         | 
| 212 | 
            +
                      klass, field, memoized(safe_ivar_name, setter, retriever)
         | 
| 213 | 
            +
                    )
         | 
| 214 | 
            +
                  end
         | 
| 215 | 
            +
             | 
| 216 | 
            +
                  private
         | 
| 217 | 
            +
             | 
| 218 | 
            +
                  attr_reader :field, :retriever, :setter_factory
         | 
| 219 | 
            +
             | 
| 220 | 
            +
                  def memoized(ivar_name, setter, retriever)
         | 
| 221 | 
            +
                    <<-MEMOIZED
         | 
| 222 | 
            +
                      return #{ivar_name} if defined?(#{ivar_name})
         | 
| 223 | 
            +
                      new_value = begin
         | 
| 224 | 
            +
                        #{retriever}
         | 
| 137 225 | 
             
                      end
         | 
| 138 | 
            -
             | 
| 226 | 
            +
                      #{setter}
         | 
| 227 | 
            +
                    MEMOIZED
         | 
| 139 228 | 
             
                  end
         | 
| 140 229 |  | 
| 141 | 
            -
                  def  | 
| 142 | 
            -
                     | 
| 143 | 
            -
             | 
| 144 | 
            -
             | 
| 145 | 
            -
                         | 
| 146 | 
            -
             | 
| 147 | 
            -
                        nil
         | 
| 148 | 
            -
                      end || (fallback && send(fallback))
         | 
| 230 | 
            +
                  def safe_ivar_name
         | 
| 231 | 
            +
                    @safe_ivar_name ||= begin
         | 
| 232 | 
            +
                      "@business_flow_dsl_#{field}"
         | 
| 233 | 
            +
                        .sub(/\?$/, '_query')
         | 
| 234 | 
            +
                        .sub(/\!$/, '_bang')
         | 
| 235 | 
            +
                        .to_sym
         | 
| 149 236 | 
             
                    end
         | 
| 150 237 | 
             
                  end
         | 
| 238 | 
            +
                end
         | 
| 239 | 
            +
             | 
| 240 | 
            +
                # Responsible for declaring fields which will be memoized and validated
         | 
| 241 | 
            +
                # when first set
         | 
| 242 | 
            +
                class UsesField
         | 
| 243 | 
            +
                  def initialize(field, retriever)
         | 
| 244 | 
            +
                    @name = field
         | 
| 245 | 
            +
                    @retriever = retriever
         | 
| 246 | 
            +
                    @field = MemoizedField.new(field, retriever_method_name,
         | 
| 247 | 
            +
                                               Field.method(:setter_factory))
         | 
| 248 | 
            +
                  end
         | 
| 151 249 |  | 
| 152 | 
            -
                  def  | 
| 153 | 
            -
                     | 
| 154 | 
            -
                     | 
| 250 | 
            +
                  def add_to(klass)
         | 
| 251 | 
            +
                    klass.send(:define_method, retriever_method_name, &@retriever)
         | 
| 252 | 
            +
                    klass.send(:private, retriever_method_name)
         | 
| 253 | 
            +
                    @field.add_to(klass)
         | 
| 155 254 | 
             
                  end
         | 
| 156 255 |  | 
| 157 | 
            -
                   | 
| 158 | 
            -
             | 
| 159 | 
            -
             | 
| 160 | 
            -
                     | 
| 256 | 
            +
                  private
         | 
| 257 | 
            +
             | 
| 258 | 
            +
                  def retriever_method_name
         | 
| 259 | 
            +
                    @retriever_method_name ||=
         | 
| 260 | 
            +
                      "_business_flow_dsl_execute_step_for_#{@name}".to_sym
         | 
| 161 261 | 
             
                  end
         | 
| 262 | 
            +
                end
         | 
| 162 263 |  | 
| 163 | 
            -
             | 
| 164 | 
            -
             | 
| 165 | 
            -
             | 
| 166 | 
            -
                     | 
| 167 | 
            -
                     | 
| 168 | 
            -
                     | 
| 264 | 
            +
                # Helper class around input parameter fields
         | 
| 265 | 
            +
                class ParameterField
         | 
| 266 | 
            +
                  def initialize(field, fallback = nil)
         | 
| 267 | 
            +
                    retriever = "@parameter_object.fetch(:#{field})"
         | 
| 268 | 
            +
                    retriever += " { send(:#{fallback}) }" if fallback
         | 
| 269 | 
            +
                    @field = MemoizedField.new(field, retriever, method(:setter_factory))
         | 
| 169 270 | 
             
                  end
         | 
| 170 271 |  | 
| 171 | 
            -
                  def  | 
| 172 | 
            -
                     | 
| 173 | 
            -
                              klass.private_method_defined?(field)
         | 
| 174 | 
            -
                    klass.send(:attr_reader, field)
         | 
| 272 | 
            +
                  def add_to(klass)
         | 
| 273 | 
            +
                    @field.add_to(klass)
         | 
| 175 274 | 
             
                  end
         | 
| 176 275 |  | 
| 177 | 
            -
                   | 
| 178 | 
            -
             | 
| 179 | 
            -
             | 
| 180 | 
            -
                     | 
| 181 | 
            -
                               &setter_proc("@#{field}", field))
         | 
| 276 | 
            +
                  private
         | 
| 277 | 
            +
             | 
| 278 | 
            +
                  def setter_factory(_field, ivar_name)
         | 
| 279 | 
            +
                    "#{ivar_name} = new_value"
         | 
| 182 280 | 
             
                  end
         | 
| 281 | 
            +
                end
         | 
| 183 282 |  | 
| 184 | 
            -
             | 
| 185 | 
            -
             | 
| 186 | 
            -
             | 
| 187 | 
            -
             | 
| 188 | 
            -
                    elsif ivar_name.end_with?('!')
         | 
| 189 | 
            -
                      ivar_name.sub!(/\!$/, '_bang')
         | 
| 190 | 
            -
                    end
         | 
| 191 | 
            -
                    ivar_name.to_sym
         | 
| 283 | 
            +
                # Manage logic around input parameters
         | 
| 284 | 
            +
                class ParameterObject
         | 
| 285 | 
            +
                  def initialize(parameters)
         | 
| 286 | 
            +
                    @parameters = parameters
         | 
| 192 287 | 
             
                  end
         | 
| 193 288 |  | 
| 194 | 
            -
                  def  | 
| 195 | 
            -
                     | 
| 196 | 
            -
             | 
| 197 | 
            -
             | 
| 198 | 
            -
                      new_value
         | 
| 199 | 
            -
                    end
         | 
| 289 | 
            +
                  def fetch(key)
         | 
| 290 | 
            +
                    value = inner_fetch(key)
         | 
| 291 | 
            +
                    return yield if !value && block_given?
         | 
| 292 | 
            +
                    value
         | 
| 200 293 | 
             
                  end
         | 
| 201 294 |  | 
| 202 | 
            -
                   | 
| 203 | 
            -
             | 
| 204 | 
            -
             | 
| 205 | 
            -
                     | 
| 206 | 
            -
                       | 
| 207 | 
            -
             | 
| 208 | 
            -
                       | 
| 209 | 
            -
                        instance_exec(step.call(self).output, &setter_proc)
         | 
| 210 | 
            -
                      end
         | 
| 295 | 
            +
                  private
         | 
| 296 | 
            +
             | 
| 297 | 
            +
                  def inner_fetch(key)
         | 
| 298 | 
            +
                    if @parameters.is_a?(Hash) && @parameters.key?(key)
         | 
| 299 | 
            +
                      @parameters[key]
         | 
| 300 | 
            +
                    else
         | 
| 301 | 
            +
                      @parameters.public_send(key)
         | 
| 211 302 | 
             
                    end
         | 
| 303 | 
            +
                  rescue NoMethodError
         | 
| 304 | 
            +
                    nil
         | 
| 212 305 | 
             
                  end
         | 
| 213 306 | 
             
                end
         | 
| 214 307 | 
             
              end
         | 
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            module BusinessFlow
         | 
| 2 | 
            +
              # Include me to fire ActiveSupport notifications on the flow, every step,
         | 
| 3 | 
            +
              # and for any errors that happen.
         | 
| 4 | 
            +
              module Instrument
         | 
| 5 | 
            +
                def self.included(klass)
         | 
| 6 | 
            +
                  klass.extend(ClassMethods)
         | 
| 7 | 
            +
                  klass.step_executor ::BusinessFlow::InstrumentedExecutor
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                # Contains methods that we add to the DSL
         | 
| 11 | 
            +
                module ClassMethods
         | 
| 12 | 
            +
                  def instrument_steps
         | 
| 13 | 
            +
                    step_executor ::BusinessFlow::InstrumentedStepExecutor
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
| @@ -0,0 +1,28 @@ | |
| 1 | 
            +
            module BusinessFlow
         | 
| 2 | 
            +
              # Fire ActiveSupport events for every step that's run and on errors
         | 
| 3 | 
            +
              class InstrumentedExecutor < DefaultStepExecutor
         | 
| 4 | 
            +
                def call
         | 
| 5 | 
            +
                  name = flow_event_name
         | 
| 6 | 
            +
                  payload = { flow: flow }
         | 
| 7 | 
            +
                  ActiveSupport::Notifications.instrument(name, payload) do
         | 
| 8 | 
            +
                    super
         | 
| 9 | 
            +
                  end
         | 
| 10 | 
            +
                  notify_errors(name, payload)
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                protected
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def notify_errors(name, payload)
         | 
| 16 | 
            +
                  return unless flow.errors?
         | 
| 17 | 
            +
                  ActiveSupport::Notifications.publish(name + '.error', payload)
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def flow_name
         | 
| 21 | 
            +
                  @flow_name ||= flow.class.to_s.underscore
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def flow_event_name
         | 
| 25 | 
            +
                  @flow_event_name ||= "business_flow.flow.#{flow_name}"
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
            end
         | 
| @@ -0,0 +1,23 @@ | |
| 1 | 
            +
            module BusinessFlow
         | 
| 2 | 
            +
              # Fire ActiveSupport events for every step that's run and on errors
         | 
| 3 | 
            +
              class InstrumentedStepExecutor < InstrumentedExecutor
         | 
| 4 | 
            +
                protected
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                def execute_step(step)
         | 
| 7 | 
            +
                  i_name = event_name(step)
         | 
| 8 | 
            +
                  i_payload = { flow: flow, step: step }
         | 
| 9 | 
            +
                  ActiveSupport::Notifications.instrument(i_name, i_payload) do |payload|
         | 
| 10 | 
            +
                    payload[:step_result] = super
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
                  notify_errors(i_name, i_payload)
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def step_event_name(step)
         | 
| 16 | 
            +
                  "#{flow_name}.#{step.to_s.underscore}"
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def event_name(step)
         | 
| 20 | 
            +
                  "business_flow.step.#{step_event_name(step)}"
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
              end
         | 
| 23 | 
            +
            end
         | 
    
        data/lib/business_flow/step.rb
    CHANGED
    
    | @@ -35,19 +35,17 @@ module BusinessFlow | |
| 35 35 | 
             
                # Represents the result of a step, and allows setting response values on
         | 
| 36 36 | 
             
                # an object, and merging error data into the same object.
         | 
| 37 37 | 
             
                class Result
         | 
| 38 | 
            +
                  # :reek:ManualDispatch Checking respond_to? is signficantly faster than
         | 
| 39 | 
            +
                  # eating the NoMethodError when grabbing our error object.
         | 
| 38 40 | 
             
                  def initialize(result, output_map, output_callable)
         | 
| 39 41 | 
             
                    @result = result
         | 
| 40 42 | 
             
                    @output_map = output_map
         | 
| 41 43 | 
             
                    @output_callable = output_callable
         | 
| 42 | 
            -
                    @result_errors =  | 
| 43 | 
            -
                                       result.errors
         | 
| 44 | 
            -
                                     rescue NoMethodError
         | 
| 45 | 
            -
                                       nil
         | 
| 46 | 
            -
                                     end
         | 
| 44 | 
            +
                    @result_errors = result.respond_to?(:errors) ? result.errors : nil
         | 
| 47 45 | 
             
                  end
         | 
| 48 46 |  | 
| 49 47 | 
             
                  def merge_into(object)
         | 
| 50 | 
            -
                    merge_errors_into(object | 
| 48 | 
            +
                    merge_errors_into(object)
         | 
| 51 49 | 
             
                    merge_outputs_into(object)
         | 
| 52 50 | 
             
                  end
         | 
| 53 51 |  | 
| @@ -74,11 +72,11 @@ module BusinessFlow | |
| 74 72 |  | 
| 75 73 | 
             
                  private
         | 
| 76 74 |  | 
| 77 | 
            -
                  def merge_errors_into( | 
| 75 | 
            +
                  def merge_errors_into(object)
         | 
| 78 76 | 
             
                    return if @result_errors.blank?
         | 
| 79 77 | 
             
                    @result_errors.each do |attribute, message|
         | 
| 80 78 | 
             
                      attribute = "#{@result.class.name.underscore}.#{attribute}"
         | 
| 81 | 
            -
                      (errors[attribute] << message).uniq!
         | 
| 79 | 
            +
                      (object.errors[attribute] << message).uniq!
         | 
| 82 80 | 
             
                    end
         | 
| 83 81 | 
             
                    throw :halt_step
         | 
| 84 82 | 
             
                  end
         | 
| @@ -111,13 +109,56 @@ module BusinessFlow | |
| 111 109 | 
             
                  end
         | 
| 112 110 | 
             
                end
         | 
| 113 111 |  | 
| 112 | 
            +
                # Handle some logic around conditions
         | 
| 113 | 
            +
                class ConditionList
         | 
| 114 | 
            +
                  def initialize(if_stmts, unless_stmts)
         | 
| 115 | 
            +
                    @conditions = Array.wrap(if_stmts).map(&Callable.method(:new)) +
         | 
| 116 | 
            +
                                  Array.wrap(unless_stmts).map(&method(:to_unless))
         | 
| 117 | 
            +
                  end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                  def call(instance, inputs)
         | 
| 120 | 
            +
                    @conditions.all? { |cond| cond.call(instance, inputs) }
         | 
| 121 | 
            +
                  end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                  private
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                  def to_unless(cond)
         | 
| 126 | 
            +
                    if_stmt = Callable.new(cond)
         | 
| 127 | 
            +
                    unless_stmt = proc do |instance, input|
         | 
| 128 | 
            +
                      !if_stmt.call(instance, input)
         | 
| 129 | 
            +
                    end
         | 
| 130 | 
            +
                    Callable.new(unless_stmt)
         | 
| 131 | 
            +
                  end
         | 
| 132 | 
            +
                end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                # Responsible for creating objects based on our input options
         | 
| 135 | 
            +
                Options = Struct.new(:opts) do
         | 
| 136 | 
            +
                  def input_object
         | 
| 137 | 
            +
                    Inputs.new(opts[:inputs] || {})
         | 
| 138 | 
            +
                  end
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                  def result_factory
         | 
| 141 | 
            +
                    ResultFactory.new(opts[:outputs] || {},
         | 
| 142 | 
            +
                                      opts[:output] || ->(result) { result })
         | 
| 143 | 
            +
                  end
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                  def condition
         | 
| 146 | 
            +
                    opts.fetch(:condition) do
         | 
| 147 | 
            +
                      if_stmts = opts[:if]
         | 
| 148 | 
            +
                      unless_stmts = opts[:unless]
         | 
| 149 | 
            +
                      if if_stmts.present? || unless_stmts.present?
         | 
| 150 | 
            +
                        ConditionList.new(if_stmts, unless_stmts)
         | 
| 151 | 
            +
                      end
         | 
| 152 | 
            +
                    end || proc { true }
         | 
| 153 | 
            +
                  end
         | 
| 154 | 
            +
                end
         | 
| 155 | 
            +
             | 
| 114 156 | 
             
                def initialize(callable, opts)
         | 
| 115 157 | 
             
                  @callable = callable
         | 
| 116 | 
            -
                   | 
| 117 | 
            -
                   | 
| 118 | 
            -
                   | 
| 119 | 
            -
                  @ | 
| 120 | 
            -
                  @condition = opts[:condition] || proc { true }
         | 
| 158 | 
            +
                  opts = Options.new(opts)
         | 
| 159 | 
            +
                  @input_object = opts.input_object
         | 
| 160 | 
            +
                  @result_factory = opts.result_factory
         | 
| 161 | 
            +
                  @condition = opts.condition
         | 
| 121 162 | 
             
                end
         | 
| 122 163 |  | 
| 123 164 | 
             
                def call(parameter_source)
         | 
| @@ -0,0 +1,25 @@ | |
| 1 | 
            +
            module BusinessFlow
         | 
| 2 | 
            +
              # Responsible for adding validations to flow objects
         | 
| 3 | 
            +
              module Validations
         | 
| 4 | 
            +
                # Additions to the DSL
         | 
| 5 | 
            +
                module ClassMethods
         | 
| 6 | 
            +
                  # Declares that you expect to set this field during the course of
         | 
| 7 | 
            +
                  # processing, and that it should meet the given ActiveModel
         | 
| 8 | 
            +
                  # validations.
         | 
| 9 | 
            +
                  def expects(field, options = {})
         | 
| 10 | 
            +
                    validates field, options.merge(on: field)
         | 
| 11 | 
            +
                    ::BusinessFlow::DSL::Field.new(field).add_to(self)
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def self.included(klass)
         | 
| 16 | 
            +
                  klass.include(ActiveModel::Validations)
         | 
| 17 | 
            +
                  klass.class_eval do
         | 
| 18 | 
            +
                    class << self
         | 
| 19 | 
            +
                      alias_method :invariant, :validates
         | 
| 20 | 
            +
                    end
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
                  klass.extend ClassMethods
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
              end
         | 
| 25 | 
            +
            end
         | 
    
        data/lib/business_flow.rb
    CHANGED
    
    | @@ -1,14 +1,17 @@ | |
| 1 1 | 
             
            require 'active_model'
         | 
| 2 2 | 
             
            require 'active_support/core_ext'
         | 
| 3 3 | 
             
            require 'business_flow/version'
         | 
| 4 | 
            -
            require 'business_flow/not_nil_validator'
         | 
| 5 4 | 
             
            require 'business_flow/flow_failed_exception'
         | 
| 6 5 | 
             
            require 'business_flow/callable'
         | 
| 7 6 | 
             
            require 'business_flow/step'
         | 
| 8 7 | 
             
            require 'business_flow/default_step_executor'
         | 
| 8 | 
            +
            require 'business_flow/instrumented_executor'
         | 
| 9 | 
            +
            require 'business_flow/instrumented_step_executor'
         | 
| 9 10 | 
             
            require 'business_flow/dsl'
         | 
| 11 | 
            +
            require 'business_flow/validations'
         | 
| 10 12 | 
             
            require 'business_flow/base'
         | 
| 11 13 | 
             
            require 'business_flow/cacheable'
         | 
| 14 | 
            +
            require 'business_flow/instrument'
         | 
| 12 15 |  | 
| 13 16 | 
             
            # Makes the magic happen.
         | 
| 14 17 | 
             
            module BusinessFlow
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: business_flow
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.8.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Alex Scarborough
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: exe
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2018- | 
| 11 | 
            +
            date: 2018-04-02 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: activemodel
         | 
| @@ -175,8 +175,11 @@ files: | |
| 175 175 | 
             
            - lib/business_flow/default_step_executor.rb
         | 
| 176 176 | 
             
            - lib/business_flow/dsl.rb
         | 
| 177 177 | 
             
            - lib/business_flow/flow_failed_exception.rb
         | 
| 178 | 
            -
            - lib/business_flow/ | 
| 178 | 
            +
            - lib/business_flow/instrument.rb
         | 
| 179 | 
            +
            - lib/business_flow/instrumented_executor.rb
         | 
| 180 | 
            +
            - lib/business_flow/instrumented_step_executor.rb
         | 
| 179 181 | 
             
            - lib/business_flow/step.rb
         | 
| 182 | 
            +
            - lib/business_flow/validations.rb
         | 
| 180 183 | 
             
            - lib/business_flow/version.rb
         | 
| 181 184 | 
             
            homepage: https://teak.io
         | 
| 182 185 | 
             
            licenses:
         | 
| @@ -1,14 +0,0 @@ | |
| 1 | 
            -
            module BusinessFlow
         | 
| 2 | 
            -
              # Validate that a given value is not nil, but allow blank/empty values.
         | 
| 3 | 
            -
              class NotNilValidator < ActiveModel::Validator
         | 
| 4 | 
            -
                # :reek:NilCheck :reek:UtilityFunction
         | 
| 5 | 
            -
                # Dear reek -- I didn't decided the ActiveModel Validator API, I just
         | 
| 6 | 
            -
                # have to live with it.
         | 
| 7 | 
            -
                def validate(record)
         | 
| 8 | 
            -
                  record.class.needs.each do |attribute|
         | 
| 9 | 
            -
                    value = record.read_attribute_for_validation(attribute)
         | 
| 10 | 
            -
                    record.errors[attribute] << 'must not be nil' if value.nil?
         | 
| 11 | 
            -
                  end
         | 
| 12 | 
            -
                end
         | 
| 13 | 
            -
              end
         | 
| 14 | 
            -
            end
         |